@decocms/start 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +32 -135
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +2 -5
- package/.cursor/rules/migration-tooling-policy.mdc +32 -48
- package/CODEOWNERS +4 -12
- package/MIGRATION_TOOLING_PLAN.md +9 -13
- package/package.json +2 -3
- package/scripts/migrate/phase-scaffold.ts +9 -12
- package/scripts/migrate/phase-verify.ts +3 -5
- package/scripts/migrate/templates/package-json.ts +6 -9
- package/.github/workflows/deploy.yml +0 -141
- package/.github/workflows/preview.yml +0 -200
- package/.github/workflows/sync-secrets.yml +0 -171
- package/deploy/README.md +0 -121
- package/deploy/wrangler-template.jsonc +0 -46
- package/scripts/deploy/build-wrangler-config.mjs +0 -47
- package/scripts/deploy/jsonc.mjs +0 -76
- package/scripts/deploy/site-registry.mjs +0 -95
- package/scripts/deploy/wrangler-wrapper.mjs +0 -118
- package/scripts/migrate/templates/github-workflows.ts +0 -159
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
name: deploy (central)
|
|
2
|
-
|
|
3
|
-
# Reusable workflow that drives `wrangler deploy` for any storefront repo.
|
|
4
|
-
# Worker name is the storefront repo basename by convention; there is no
|
|
5
|
-
# per-site registry. The deploy is gated by the `decocms-deployer` GitHub App
|
|
6
|
-
# being installed on the target storefront repo -- the App-token mint fails
|
|
7
|
-
# (and the deploy never starts) if the App isn't installed there.
|
|
8
|
-
#
|
|
9
|
-
# v3 architecture (D6.2): triggered via `workflow_dispatch` from the storefront,
|
|
10
|
-
# authenticated as the `decocms-deployer` GitHub App. The deploy runs IN THIS
|
|
11
|
-
# REPO'S CONTEXT, so `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` resolve
|
|
12
|
-
# from this repo's plain repo secrets and never leave decocms/deco-start.
|
|
13
|
-
#
|
|
14
|
-
# Caller usage (in the storefront repo, `.github/workflows/deploy.yml`):
|
|
15
|
-
#
|
|
16
|
-
# on:
|
|
17
|
-
# push:
|
|
18
|
-
# branches: [main]
|
|
19
|
-
# permissions:
|
|
20
|
-
# contents: read
|
|
21
|
-
# jobs:
|
|
22
|
-
# trigger:
|
|
23
|
-
# runs-on: ubuntu-latest
|
|
24
|
-
# steps:
|
|
25
|
-
# - uses: actions/create-github-app-token@v1
|
|
26
|
-
# id: app-token
|
|
27
|
-
# with:
|
|
28
|
-
# app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
29
|
-
# private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
30
|
-
# owner: decocms
|
|
31
|
-
# repositories: deco-start
|
|
32
|
-
# - env:
|
|
33
|
-
# GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
34
|
-
# run: |
|
|
35
|
-
# gh workflow run deploy.yml \
|
|
36
|
-
# --repo decocms/deco-start \
|
|
37
|
-
# --ref v3 \
|
|
38
|
-
# -f site_owner=${GITHUB_REPOSITORY%%/*} \
|
|
39
|
-
# -f site_name=${GITHUB_REPOSITORY##*/}
|
|
40
|
-
|
|
41
|
-
on:
|
|
42
|
-
workflow_dispatch:
|
|
43
|
-
inputs:
|
|
44
|
-
site_owner:
|
|
45
|
-
description: "GitHub org of the storefront (e.g. deco-sites). Defaults to deco-sites."
|
|
46
|
-
type: string
|
|
47
|
-
required: false
|
|
48
|
-
default: deco-sites
|
|
49
|
-
site_name:
|
|
50
|
-
description: "Storefront repo basename. Becomes the Cloudflare worker name."
|
|
51
|
-
type: string
|
|
52
|
-
required: true
|
|
53
|
-
|
|
54
|
-
permissions:
|
|
55
|
-
contents: read
|
|
56
|
-
|
|
57
|
-
concurrency:
|
|
58
|
-
group: deploy-${{ inputs.site_owner }}-${{ inputs.site_name }}
|
|
59
|
-
cancel-in-progress: false
|
|
60
|
-
|
|
61
|
-
jobs:
|
|
62
|
-
deploy:
|
|
63
|
-
runs-on: ubuntu-latest
|
|
64
|
-
steps:
|
|
65
|
-
- name: Checkout deco-start (template + scripts)
|
|
66
|
-
uses: actions/checkout@v4
|
|
67
|
-
|
|
68
|
-
- name: Mint App token for storefront checkout
|
|
69
|
-
id: app-token
|
|
70
|
-
uses: actions/create-github-app-token@v1
|
|
71
|
-
with:
|
|
72
|
-
app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
73
|
-
private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
74
|
-
owner: ${{ inputs.site_owner }}
|
|
75
|
-
repositories: ${{ inputs.site_name }}
|
|
76
|
-
|
|
77
|
-
# SECURITY: production deploys IGNORE any caller-supplied sha. The deploy
|
|
78
|
-
# always targets the storefront's CURRENT default-branch HEAD. This means
|
|
79
|
-
# an attacker with push to repo A who triggers a deploy of repo B can
|
|
80
|
-
# only force a no-op redeploy of B's current main -- they cannot select
|
|
81
|
-
# an arbitrary historical commit (no force-rollback attack).
|
|
82
|
-
- name: Resolve target sha (storefront default branch HEAD)
|
|
83
|
-
id: target
|
|
84
|
-
env:
|
|
85
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
86
|
-
SITE_REPO: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
87
|
-
run: |
|
|
88
|
-
set -euo pipefail
|
|
89
|
-
DEFAULT_BRANCH=$(gh api "repos/$SITE_REPO" --jq .default_branch)
|
|
90
|
-
SHA=$(gh api "repos/$SITE_REPO/branches/$DEFAULT_BRANCH" --jq .commit.sha)
|
|
91
|
-
echo "ref=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT"
|
|
92
|
-
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
93
|
-
echo "::notice::Deploying $SITE_REPO @ $DEFAULT_BRANCH ($SHA)"
|
|
94
|
-
|
|
95
|
-
- name: Checkout storefront at default-branch HEAD
|
|
96
|
-
uses: actions/checkout@v4
|
|
97
|
-
with:
|
|
98
|
-
repository: ${{ inputs.site_owner }}/${{ inputs.site_name }}
|
|
99
|
-
ref: ${{ steps.target.outputs.sha }}
|
|
100
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
101
|
-
path: site
|
|
102
|
-
fetch-depth: 1
|
|
103
|
-
|
|
104
|
-
- uses: actions/setup-node@v4
|
|
105
|
-
with:
|
|
106
|
-
node-version: 22
|
|
107
|
-
|
|
108
|
-
- name: Restore npm cache
|
|
109
|
-
uses: actions/cache@v4
|
|
110
|
-
with:
|
|
111
|
-
path: ~/.npm
|
|
112
|
-
key: npm-${{ runner.os }}-${{ hashFiles('site/package-lock.json') }}
|
|
113
|
-
restore-keys: npm-${{ runner.os }}-
|
|
114
|
-
|
|
115
|
-
- name: Install dependencies
|
|
116
|
-
working-directory: site
|
|
117
|
-
run: |
|
|
118
|
-
if [ ! -f package-lock.json ]; then
|
|
119
|
-
npm install --package-lock-only
|
|
120
|
-
fi
|
|
121
|
-
npm ci
|
|
122
|
-
|
|
123
|
-
- name: Build
|
|
124
|
-
working-directory: site
|
|
125
|
-
run: npm run build
|
|
126
|
-
|
|
127
|
-
- name: Generate wrangler.jsonc from template
|
|
128
|
-
working-directory: site
|
|
129
|
-
env:
|
|
130
|
-
DECO_START_PATH: "${{ github.workspace }}"
|
|
131
|
-
WORKER_NAME: ${{ inputs.site_name }}
|
|
132
|
-
OUTPUT_PATH: "./wrangler.jsonc"
|
|
133
|
-
run: node "$DECO_START_PATH/scripts/deploy/build-wrangler-config.mjs"
|
|
134
|
-
|
|
135
|
-
- name: Deploy to Cloudflare Workers
|
|
136
|
-
working-directory: site
|
|
137
|
-
env:
|
|
138
|
-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
139
|
-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
140
|
-
BUILD_HASH: ${{ steps.target.outputs.sha }}
|
|
141
|
-
run: npx wrangler deploy --var "BUILD_HASH:${BUILD_HASH:0:7}"
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
name: preview (central)
|
|
2
|
-
|
|
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.
|
|
7
|
-
#
|
|
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`):
|
|
13
|
-
#
|
|
14
|
-
# on:
|
|
15
|
-
# pull_request:
|
|
16
|
-
# types: [opened, synchronize, reopened]
|
|
17
|
-
# push:
|
|
18
|
-
# branches: ['env/**']
|
|
19
|
-
# permissions:
|
|
20
|
-
# contents: read
|
|
21
|
-
# jobs:
|
|
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.
|
|
56
|
-
|
|
57
|
-
on:
|
|
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: ""
|
|
82
|
-
|
|
83
|
-
permissions:
|
|
84
|
-
contents: read
|
|
85
|
-
|
|
86
|
-
concurrency:
|
|
87
|
-
group: preview-${{ inputs.site_owner }}-${{ inputs.site_name }}-${{ inputs.alias }}
|
|
88
|
-
cancel-in-progress: true
|
|
89
|
-
|
|
90
|
-
jobs:
|
|
91
|
-
preview:
|
|
92
|
-
runs-on: ubuntu-latest
|
|
93
|
-
steps:
|
|
94
|
-
- name: Checkout deco-start (template + scripts)
|
|
95
|
-
uses: actions/checkout@v4
|
|
96
|
-
|
|
97
|
-
- name: Validate alias format
|
|
98
|
-
env:
|
|
99
|
-
ALIAS: ${{ inputs.alias }}
|
|
100
|
-
run: |
|
|
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
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
- name: Mint App token for storefront checkout
|
|
108
|
-
id: app-token
|
|
109
|
-
uses: actions/create-github-app-token@v1
|
|
110
|
-
with:
|
|
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 }}
|
|
115
|
-
|
|
116
|
-
- name: Checkout storefront at requested sha
|
|
117
|
-
uses: actions/checkout@v4
|
|
118
|
-
with:
|
|
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
|
|
124
|
-
|
|
125
|
-
- uses: actions/setup-node@v4
|
|
126
|
-
with:
|
|
127
|
-
node-version: 22
|
|
128
|
-
|
|
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 }}-
|
|
135
|
-
|
|
136
|
-
- name: Install dependencies
|
|
137
|
-
working-directory: site
|
|
138
|
-
run: |
|
|
139
|
-
if [ ! -f package-lock.json ]; then
|
|
140
|
-
npm install --package-lock-only
|
|
141
|
-
fi
|
|
142
|
-
npm ci
|
|
143
|
-
|
|
144
|
-
- name: Build
|
|
145
|
-
working-directory: site
|
|
146
|
-
run: npm run build
|
|
147
|
-
|
|
148
|
-
- name: Generate wrangler.jsonc from template
|
|
149
|
-
working-directory: site
|
|
150
|
-
env:
|
|
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"
|
|
155
|
-
|
|
156
|
-
- name: Upload preview version
|
|
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 }}
|
|
163
|
-
run: |
|
|
164
|
-
set +e
|
|
165
|
-
OUTPUT=$(npx wrangler versions upload --preview-alias "$ALIAS" 2>&1)
|
|
166
|
-
EXIT_CODE=$?
|
|
167
|
-
set -e
|
|
168
|
-
echo "$OUTPUT"
|
|
169
|
-
if [ $EXIT_CODE -ne 0 ]; then
|
|
170
|
-
echo "::error::wrangler versions upload failed with exit code $EXIT_CODE"
|
|
171
|
-
exit $EXIT_CODE
|
|
172
|
-
fi
|
|
173
|
-
PREVIEW_URL=$(echo "$OUTPUT" | grep 'Version Preview URL:' | sed 's/.*Version Preview URL: //')
|
|
174
|
-
ALIAS_URL=$(echo "$OUTPUT" | grep 'Version Preview Alias URL:' | sed 's/.*Version Preview Alias URL: //')
|
|
175
|
-
echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
|
|
176
|
-
echo "alias_url=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
|
|
177
|
-
|
|
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
|
|
182
|
-
with:
|
|
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,171 +0,0 @@
|
|
|
1
|
-
name: sync-secrets (central)
|
|
2
|
-
|
|
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).
|
|
6
|
-
#
|
|
7
|
-
# Examples:
|
|
8
|
-
# SECRET_STRIPE_KEY in deco-start env -> STRIPE_KEY on the worker
|
|
9
|
-
#
|
|
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
|
|
19
|
-
# warned about, never deleted.
|
|
20
|
-
#
|
|
21
|
-
# Caller usage (in the storefront repo, `.github/workflows/sync-secrets.yml`):
|
|
22
|
-
#
|
|
23
|
-
# on:
|
|
24
|
-
# workflow_dispatch:
|
|
25
|
-
# inputs:
|
|
26
|
-
# mode:
|
|
27
|
-
# type: choice
|
|
28
|
-
# options: [dry-run, apply]
|
|
29
|
-
# default: dry-run
|
|
30
|
-
# permissions:
|
|
31
|
-
# contents: read
|
|
32
|
-
# jobs:
|
|
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 }}
|
|
51
|
-
|
|
52
|
-
on:
|
|
53
|
-
workflow_dispatch:
|
|
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
|
|
59
|
-
mode:
|
|
60
|
-
description: "dry-run = print diff only | apply = set secrets on worker"
|
|
61
|
-
type: string
|
|
62
|
-
required: false
|
|
63
|
-
default: "dry-run"
|
|
64
|
-
|
|
65
|
-
permissions:
|
|
66
|
-
contents: read
|
|
67
|
-
|
|
68
|
-
concurrency:
|
|
69
|
-
group: sync-secrets-${{ inputs.site_name }}
|
|
70
|
-
cancel-in-progress: false
|
|
71
|
-
|
|
72
|
-
jobs:
|
|
73
|
-
sync:
|
|
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
|
|
79
|
-
steps:
|
|
80
|
-
- name: Checkout deco-start (template + scripts)
|
|
81
|
-
uses: actions/checkout@v4
|
|
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
|
|
89
|
-
|
|
90
|
-
- uses: actions/setup-node@v4
|
|
91
|
-
with:
|
|
92
|
-
node-version: 22
|
|
93
|
-
|
|
94
|
-
- name: Install wrangler
|
|
95
|
-
run: npm install --no-save wrangler@4
|
|
96
|
-
|
|
97
|
-
- name: Plan and (optionally) apply
|
|
98
|
-
env:
|
|
99
|
-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
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.
|
|
103
|
-
ALL_SECRETS: ${{ toJSON(secrets) }}
|
|
104
|
-
MODE: ${{ inputs.mode }}
|
|
105
|
-
run: |
|
|
106
|
-
set -euo pipefail
|
|
107
|
-
|
|
108
|
-
desired_json=$(printf '%s' "$ALL_SECRETS" | jq '
|
|
109
|
-
to_entries
|
|
110
|
-
| map(select(.key | startswith("SECRET_")))
|
|
111
|
-
| map({ key: (.key | sub("^SECRET_"; "")), value: .value })
|
|
112
|
-
| from_entries
|
|
113
|
-
')
|
|
114
|
-
|
|
115
|
-
desired_names=$(printf '%s' "$desired_json" | jq -r 'keys[]' | sort)
|
|
116
|
-
|
|
117
|
-
while IFS= read -r name; do
|
|
118
|
-
[ -z "$name" ] && continue
|
|
119
|
-
if ! [[ "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
|
|
120
|
-
echo "::error::Invalid worker secret name derived from SECRET_${name}: must match ^[A-Z][A-Z0-9_]{0,63}$"
|
|
121
|
-
exit 1
|
|
122
|
-
fi
|
|
123
|
-
done <<< "$desired_names"
|
|
124
|
-
|
|
125
|
-
existing=$(npx wrangler secret list --format=json | jq -r '.[].name' | sort)
|
|
126
|
-
|
|
127
|
-
to_set="$desired_names"
|
|
128
|
-
orphans=$(comm -23 <(printf '%s\n' "$existing") <(printf '%s\n' "$desired_names") || true)
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
echo "## Diff for ${{ inputs.site_name }}"
|
|
132
|
-
echo ""
|
|
133
|
-
echo "### Will set (from SECRET_* in \`${{ inputs.site_name }}-secrets\` environment):"
|
|
134
|
-
if [ -z "$to_set" ]; then
|
|
135
|
-
echo " (none -- environment has no SECRET_* values)"
|
|
136
|
-
else
|
|
137
|
-
echo "$to_set" | sed 's/^/ + /'
|
|
138
|
-
fi
|
|
139
|
-
echo ""
|
|
140
|
-
echo "### Orphans (on worker, not in env as SECRET_*):"
|
|
141
|
-
if [ -z "$orphans" ]; then
|
|
142
|
-
echo " (none)"
|
|
143
|
-
else
|
|
144
|
-
echo "$orphans" | sed 's/^/ ! /'
|
|
145
|
-
echo ""
|
|
146
|
-
echo " These will NOT be deleted automatically. To remove manually:"
|
|
147
|
-
echo "$orphans" | sed 's|^| npx wrangler secret delete "|; s|$|" --force|'
|
|
148
|
-
fi
|
|
149
|
-
} | tee -a "$GITHUB_STEP_SUMMARY"
|
|
150
|
-
|
|
151
|
-
if [ -n "$orphans" ]; then
|
|
152
|
-
echo "::warning::${orphans//$'\n'/, } exist on the worker but not in env as SECRET_*. Not deleting."
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
if [ "$MODE" = "dry-run" ]; then
|
|
156
|
-
echo "::notice::dry-run mode -- no changes applied"
|
|
157
|
-
exit 0
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
if [ -z "$to_set" ]; then
|
|
161
|
-
echo "Nothing to apply."
|
|
162
|
-
exit 0
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
printf '%s' "$desired_json" | jq -r 'to_entries[] | "\(.key)\t\(.value)"' \
|
|
166
|
-
| while IFS=$'\t' read -r name value; do
|
|
167
|
-
echo "Setting $name..."
|
|
168
|
-
printf '%s' "$value" | npx wrangler secret put "$name"
|
|
169
|
-
done
|
|
170
|
-
|
|
171
|
-
echo "::notice::Applied $(echo "$to_set" | wc -l | tr -d ' ') secrets."
|
package/deploy/README.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# `deploy/` — central wrangler template
|
|
2
|
-
|
|
3
|
-
This directory holds **`wrangler-template.jsonc`** — the canonical wrangler config
|
|
4
|
-
that every storefront on the platform inherits. It is consumed by the reusable
|
|
5
|
-
GitHub workflows under [`.github/workflows/`](../.github/workflows/)
|
|
6
|
-
(`deploy.yml`, `preview.yml`, `sync-secrets.yml`) and by the local
|
|
7
|
-
`deco-wrangler` CLI.
|
|
8
|
-
|
|
9
|
-
There is **no per-site registry**. Worker name is the storefront repo basename
|
|
10
|
-
by convention (`deco-sites/baggagio-tanstack` → worker `baggagio-tanstack`).
|
|
11
|
-
Anything that must vary deterministically per worker (like the Analytics Engine
|
|
12
|
-
dataset name) is encoded as a substitution token in the template — see
|
|
13
|
-
[Substitution tokens](#substitution-tokens) below.
|
|
14
|
-
|
|
15
|
-
## Trust model
|
|
16
|
-
|
|
17
|
-
The deploy is gated by the `decocms-deployer` **GitHub App** being installed on
|
|
18
|
-
the target storefront repo:
|
|
19
|
-
|
|
20
|
-
1. The storefront's caller workflow mints a short-lived App-installation token.
|
|
21
|
-
2. It calls `gh workflow run deploy.yml --repo decocms/deco-start -f site_owner=… -f site_name=…`.
|
|
22
|
-
3. The central deploy workflow runs **in this repo's context** and itself mints
|
|
23
|
-
another short-lived App-installation token to check out the storefront. If
|
|
24
|
-
the App isn't installed on `<site_owner>/<site_name>`, the mint fails and
|
|
25
|
-
the deploy never starts.
|
|
26
|
-
4. The central workflow then runs build + `wrangler deploy` using
|
|
27
|
-
`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from this repo's plain
|
|
28
|
-
repo secrets.
|
|
29
|
-
|
|
30
|
-
Properties this gives:
|
|
31
|
-
|
|
32
|
-
- **CF credentials never leave decocms/deco-start.** The storefront repo holds
|
|
33
|
-
zero Cloudflare credentials — it only has the GitHub App credentials, which
|
|
34
|
-
can be used solely to trigger workflows on this repo.
|
|
35
|
-
- **Worker naming is convention-based and not customer-controlled.** A
|
|
36
|
-
customer with push access to their own storefront cannot rename the worker
|
|
37
|
-
their deploy lands on (the central workflow always uses
|
|
38
|
-
`inputs.site_name` as the worker name; modifying the caller stub to pass a
|
|
39
|
-
different `site_name` would also require the App to be installed on that
|
|
40
|
-
other repo).
|
|
41
|
-
- **Force-rollback is impossible for production.** The central deploy
|
|
42
|
-
workflow ignores any caller-supplied sha and always resolves the
|
|
43
|
-
storefront's current default-branch HEAD itself. The worst a compromised
|
|
44
|
-
storefront can do across tenants is trigger a no-op redeploy of another
|
|
45
|
-
storefront's current main.
|
|
46
|
-
|
|
47
|
-
`deploy/` and `scripts/deploy/` and the central workflow files are
|
|
48
|
-
CODEOWNERS-protected — only the platform team approves changes.
|
|
49
|
-
|
|
50
|
-
### Where Cloudflare credentials live
|
|
51
|
-
|
|
52
|
-
| Secret class | Lives in | How it reaches the worker |
|
|
53
|
-
|---|---|---|
|
|
54
|
-
| `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's **repo secrets** | central workflow runs in this repo's context, env-var resolves natively |
|
|
55
|
-
| `DECOCMS_DEPLOYER_APP_ID` / `DECOCMS_DEPLOYER_APP_PRIVATE_KEY` | this repo's repo secrets AND deco-sites org-level secrets | mints short-lived installation tokens for both directions of the dispatch flow |
|
|
56
|
-
| `SECRET_*` runtime secrets (per site) | this repo's `<site_name>-secrets` GitHub Environment | `sync-secrets.yml` binds to that environment, reads `SECRET_*` from `${{ secrets }}`, runs `wrangler secret put` |
|
|
57
|
-
|
|
58
|
-
To rotate Cloudflare credentials, edit them in this repo only. To rotate a
|
|
59
|
-
runtime secret for one storefront, edit the corresponding environment in this
|
|
60
|
-
repo only. No storefront PR needed for either.
|
|
61
|
-
|
|
62
|
-
## How `wrangler.jsonc` is generated
|
|
63
|
-
|
|
64
|
-
At deploy time, the central workflow runs
|
|
65
|
-
[`scripts/deploy/build-wrangler-config.mjs`](../scripts/deploy/build-wrangler-config.mjs),
|
|
66
|
-
which:
|
|
67
|
-
|
|
68
|
-
1. Loads `deploy/wrangler-template.jsonc`.
|
|
69
|
-
2. Substitutes `$WORKER_*` tokens (see below) using the worker name passed by
|
|
70
|
-
the central workflow (= storefront repo basename).
|
|
71
|
-
3. Writes the result to `./wrangler.jsonc` in the storefront checkout, with
|
|
72
|
-
`name` injected as the first key.
|
|
73
|
-
|
|
74
|
-
`account_id` is never written to JSON — wrangler reads it from
|
|
75
|
-
`CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally). This way a
|
|
76
|
-
typo cannot misroute a deploy to a different Cloudflare account.
|
|
77
|
-
|
|
78
|
-
### Substitution tokens
|
|
79
|
-
|
|
80
|
-
Any string in the template containing one of these literals is replaced at
|
|
81
|
-
build time:
|
|
82
|
-
|
|
83
|
-
| Token | Replacement | Example use |
|
|
84
|
-
|---|---|---|
|
|
85
|
-
| `$WORKER_NAME` | worker name verbatim | rare; mostly available for parity |
|
|
86
|
-
| `$WORKER_UNDERSCORE` | worker name with `-` → `_` | `analytics_engine_datasets[].dataset` (must be a valid Postgres-style identifier) |
|
|
87
|
-
|
|
88
|
-
To add a new derived field, add the token wherever it makes sense in
|
|
89
|
-
`wrangler-template.jsonc`. Anything not in the substitution table appears
|
|
90
|
-
verbatim in the generated config.
|
|
91
|
-
|
|
92
|
-
## Adding a new site
|
|
93
|
-
|
|
94
|
-
1. Install the `decocms-deployer` GitHub App on the new storefront repo
|
|
95
|
-
(Settings → Integrations → GitHub Apps in the deco-sites org).
|
|
96
|
-
2. Add the four caller workflow stubs to the new repo (copy from any existing
|
|
97
|
-
storefront's `.github/workflows/{deploy,preview,sync-secrets,regen-blocks}.yml`).
|
|
98
|
-
3. Add `wrangler.jsonc` to the new repo's `.gitignore` and add the
|
|
99
|
-
`gen:wrangler` / `predev` / `prebuild` / `types` scripts to `package.json`
|
|
100
|
-
so local dev still works (use any existing storefront as a template).
|
|
101
|
-
4. If the site needs runtime secrets, create a new environment in this repo
|
|
102
|
-
named `<repo-basename>-secrets` and add the `SECRET_*` values there. Set
|
|
103
|
-
environment protection rules to grant the site team self-service access to
|
|
104
|
-
their own environment.
|
|
105
|
-
5. Push to `main` and verify the deploy lands on a worker named after the repo.
|
|
106
|
-
|
|
107
|
-
## Migrating an existing site whose worker name doesn't match its repo
|
|
108
|
-
|
|
109
|
-
Two cases to be aware of:
|
|
110
|
-
|
|
111
|
-
- **Worker rename.** The worker created by the first deploy will use the repo
|
|
112
|
-
basename. If an old worker exists with a different name (e.g.
|
|
113
|
-
`miess-01-tanstack` repo whose old worker was `miess-tanstack`), you'll need
|
|
114
|
-
a manual cutover: deploy the new worker, re-attach custom domain routes via
|
|
115
|
-
the Cloudflare dashboard, copy any wrangler secrets, then delete the old
|
|
116
|
-
worker. There is intentionally no per-site override for this — these cases
|
|
117
|
-
are rare and best resolved at the CF layer.
|
|
118
|
-
- **AE dataset rename.** The dataset name is derived from worker name, so a
|
|
119
|
-
worker rename also changes the AE dataset. Old data remains queryable under
|
|
120
|
-
the old dataset name; new data goes to the new name. Update Grafana panels
|
|
121
|
-
and saved queries accordingly.
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Canonical wrangler.jsonc template for every storefront on the platform.
|
|
2
|
-
//
|
|
3
|
-
// There is no per-site registry. Worker name == storefront repo basename by
|
|
4
|
-
// convention; the central deploy workflows pass `WORKER_NAME` to the build
|
|
5
|
-
// script, which substitutes `$WORKER_*` tokens in this template and writes
|
|
6
|
-
// the result to `wrangler.jsonc` in the caller checkout.
|
|
7
|
-
//
|
|
8
|
-
// Substitution tokens (see scripts/deploy/site-registry.mjs):
|
|
9
|
-
// $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
|
|
10
|
-
// $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
|
|
11
|
-
//
|
|
12
|
-
// To upgrade compatibility flags, observability, KV bindings, or any field
|
|
13
|
-
// that should change for every site at once, change this file and tag a new
|
|
14
|
-
// deco-start release.
|
|
15
|
-
//
|
|
16
|
-
// Notes on what is INTENTIONALLY missing here:
|
|
17
|
-
// - "name" -- always derived from the per-site `worker_name`.
|
|
18
|
-
// - "account_id" -- never lives in the JSON. Wrangler reads it from the
|
|
19
|
-
// CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
|
|
20
|
-
// This way, a typo in JSON cannot misroute a deploy.
|
|
21
|
-
// - "routes" -- production custom domains are managed in the Cloudflare
|
|
22
|
-
// dashboard, not in JSON. Avoids drift between JSON and CF reality and
|
|
23
|
-
// means a site going live doesn't need a deco-start PR.
|
|
24
|
-
//
|
|
25
|
-
// To upgrade compatibility flags, observability, or worker-entry path across
|
|
26
|
-
// every site at once, change this file and tag a new deco-start release.
|
|
27
|
-
{
|
|
28
|
-
"compatibility_date": "2026-02-14",
|
|
29
|
-
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
30
|
-
"main": "./src/worker-entry.ts",
|
|
31
|
-
"workers_dev": true,
|
|
32
|
-
"preview_urls": true,
|
|
33
|
-
"kv_namespaces": [
|
|
34
|
-
{ "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
|
|
35
|
-
],
|
|
36
|
-
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
37
|
-
"analytics_engine_datasets": [
|
|
38
|
-
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_$WORKER_UNDERSCORE" }
|
|
39
|
-
],
|
|
40
|
-
"observability": {
|
|
41
|
-
"logs": {
|
|
42
|
-
"enabled": true,
|
|
43
|
-
"invocation_logs": true
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|