@stackonward/cli 1.3.3 → 1.5.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/README.md +248 -0
- package/dist/index.js +14124 -635
- package/dist/index.js.map +1 -1
- package/dist/resources/components/nuxt/site-source/app/app.vue +4 -2
- package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +50 -50
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationHeader.vue → app/Header.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationShell.vue → app/Shell.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationSidebar.vue → app/Sidebar.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutStatus.vue → checkout/Status.vue} +8 -3
- package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutSummary.vue → checkout/Summary.vue} +11 -11
- package/dist/resources/components/nuxt/site-source/app/components/{site/CookieConsent.vue → consent/Banner.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ContentArticlePage.vue → content/Article.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ContentListPage.vue → content/List.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/GoogleSignInButton.vue → google/SignInButton.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/LocaleSwitcher.vue → locale/Menu.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingFooter.vue → Footer.vue} +8 -8
- package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingHeader.vue → Header.vue} +27 -27
- package/dist/resources/components/nuxt/site-source/app/components/site/Shell.vue +29 -0
- package/dist/resources/components/nuxt/site-source/app/components/{site/ThemeSwitcher.vue → theme/Menu.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseButton.vue → Button.vue} +1 -8
- package/dist/resources/components/nuxt/site-source/app/components/ui/InfiniteScroll.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/YoutubeEmbed.vue → youtube/Embed.vue} +10 -2
- package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +8 -10
- package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +9 -14
- package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +3 -1
- package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +19 -22
- package/dist/resources/components/nuxt/site-source/app/consent/types.ts +59 -0
- package/dist/resources/components/nuxt/site-source/app/content/types.ts +17 -0
- package/dist/resources/components/nuxt/site-source/app/layouts/app.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/layouts/default.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/navigation/types.ts +68 -0
- package/dist/resources/components/nuxt/site-source/app/plugins/consent.ts +2 -2
- package/dist/resources/components/nuxt/site-source/app/site/config.ts +35 -0
- package/dist/resources/components/registry.json +63 -51
- package/dist/resources/deploy/ci/github/caller.yml +38 -0
- package/dist/resources/deploy/ci/onedev/import.yml +14 -0
- package/dist/resources/deploy/nginx/cache.conf +17 -0
- package/dist/resources/deploy/nginx/compression.conf +23 -0
- package/dist/resources/deploy/nginx/proxy.conf +15 -0
- package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
- package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
- package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
- package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
- package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
- package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
- package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
- package/dist/resources/registry.json +30 -30
- package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
- package/dist/resources/templates/base/AGENTS.md +15 -0
- package/dist/resources/templates/base/CLAUDE.md +15 -0
- package/dist/resources/templates/go-backend/.dockerignore +25 -3
- package/dist/resources/templates/go-backend/deploy.config.json +26 -0
- package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
- package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
- package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
- package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
- package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
- package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
- package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
- package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
- package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
- package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
- package/dist/resources/templates/nuxt/.dockerignore +19 -0
- package/dist/resources/templates/nuxt/README.md +3 -3
- package/dist/resources/templates/nuxt/app/app.config.ts +1 -1
- package/dist/resources/templates/nuxt/app/i18n/locales/en.json +1 -1
- package/dist/resources/templates/nuxt/app/pages/app/index.vue +1 -1
- package/dist/resources/templates/nuxt/app/pages/authors/[slug].vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/blog/[slug].vue +4 -3
- package/dist/resources/templates/nuxt/app/pages/blog/category/[category].vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/blog/index.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/legal/cookies.vue +3 -3
- package/dist/resources/templates/nuxt/app/pages/legal/privacy.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/legal/terms.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/ui/index.vue +45 -45
- package/dist/resources/templates/nuxt/deploy.config.json +27 -0
- package/dist/resources/templates/nuxt/nuxt.config.ts +4 -10
- package/dist/resources/templates/nuxt/package.json +8 -8
- package/dist/resources/templates/nuxt/test/e2e/rendering.test.ts +1 -0
- package/dist/resources/templates/nuxt/test/unit/consent.test.ts +4 -7
- package/package.json +5 -3
- package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +0 -33
- package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +0 -172
- /package/dist/resources/components/nuxt/site-source/app/components/{site/ConsentSettingsButton.vue → consent/Settings.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/{site/StaticPage.vue → content/Document.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/{site/ToastViewport.vue → feedback/Toast.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/site/{SiteBrand.vue → Brand.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseGradientBorder.vue → GradientBorder.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseModal.vue → Modal.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BasePopover.vue → Popover.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseText.vue → Text.vue} +0 -0
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
},
|
|
145
145
|
"nuxt-site-source": {
|
|
146
146
|
"name": "Nuxt Site Source",
|
|
147
|
-
"description": "Editable Nuxt product-site source:
|
|
147
|
+
"description": "Editable Nuxt product-site source: site shell, app shell, content shell, UI primitives, composables, plugins, theme tokens, and CSS.",
|
|
148
148
|
"installContext": "template",
|
|
149
149
|
"files": [
|
|
150
150
|
{
|
|
@@ -164,108 +164,108 @@
|
|
|
164
164
|
"target": "app/assets/icons/common/.gitkeep"
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
|
-
"path": "site-source/app/components/
|
|
168
|
-
"target": "app/components/
|
|
167
|
+
"path": "site-source/app/components/app/Header.vue",
|
|
168
|
+
"target": "app/components/app/Header.vue"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
|
-
"path": "site-source/app/components/
|
|
172
|
-
"target": "app/components/
|
|
171
|
+
"path": "site-source/app/components/app/Shell.vue",
|
|
172
|
+
"target": "app/components/app/Shell.vue"
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
|
-
"path": "site-source/app/components/
|
|
176
|
-
"target": "app/components/
|
|
175
|
+
"path": "site-source/app/components/app/Sidebar.vue",
|
|
176
|
+
"target": "app/components/app/Sidebar.vue"
|
|
177
177
|
},
|
|
178
178
|
{
|
|
179
179
|
"path": "site-source/app/components/site/BackToTop.vue",
|
|
180
180
|
"target": "app/components/site/BackToTop.vue"
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
|
-
"path": "site-source/app/components/
|
|
184
|
-
"target": "app/components/
|
|
183
|
+
"path": "site-source/app/components/content/Article.vue",
|
|
184
|
+
"target": "app/components/content/Article.vue"
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
|
-
"path": "site-source/app/components/
|
|
188
|
-
"target": "app/components/
|
|
187
|
+
"path": "site-source/app/components/content/List.vue",
|
|
188
|
+
"target": "app/components/content/List.vue"
|
|
189
189
|
},
|
|
190
190
|
{
|
|
191
|
-
"path": "site-source/app/components/
|
|
192
|
-
"target": "app/components/
|
|
191
|
+
"path": "site-source/app/components/consent/Settings.vue",
|
|
192
|
+
"target": "app/components/consent/Settings.vue"
|
|
193
193
|
},
|
|
194
194
|
{
|
|
195
|
-
"path": "site-source/app/components/
|
|
196
|
-
"target": "app/components/
|
|
195
|
+
"path": "site-source/app/components/consent/Banner.vue",
|
|
196
|
+
"target": "app/components/consent/Banner.vue"
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
|
-
"path": "site-source/app/components/
|
|
200
|
-
"target": "app/components/
|
|
199
|
+
"path": "site-source/app/components/locale/Menu.vue",
|
|
200
|
+
"target": "app/components/locale/Menu.vue"
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
|
-
"path": "site-source/app/components/site/
|
|
204
|
-
"target": "app/components/site/
|
|
203
|
+
"path": "site-source/app/components/site/Footer.vue",
|
|
204
|
+
"target": "app/components/site/Footer.vue"
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
|
-
"path": "site-source/app/components/site/
|
|
208
|
-
"target": "app/components/site/
|
|
207
|
+
"path": "site-source/app/components/site/Header.vue",
|
|
208
|
+
"target": "app/components/site/Header.vue"
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
|
-
"path": "site-source/app/components/site/
|
|
212
|
-
"target": "app/components/site/
|
|
211
|
+
"path": "site-source/app/components/site/Shell.vue",
|
|
212
|
+
"target": "app/components/site/Shell.vue"
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
|
-
"path": "site-source/app/components/site/
|
|
216
|
-
"target": "app/components/site/
|
|
215
|
+
"path": "site-source/app/components/site/Brand.vue",
|
|
216
|
+
"target": "app/components/site/Brand.vue"
|
|
217
217
|
},
|
|
218
218
|
{
|
|
219
|
-
"path": "site-source/app/components/
|
|
220
|
-
"target": "app/components/
|
|
219
|
+
"path": "site-source/app/components/content/Document.vue",
|
|
220
|
+
"target": "app/components/content/Document.vue"
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
|
-
"path": "site-source/app/components/
|
|
224
|
-
"target": "app/components/
|
|
223
|
+
"path": "site-source/app/components/feedback/Toast.vue",
|
|
224
|
+
"target": "app/components/feedback/Toast.vue"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
|
-
"path": "site-source/app/components/
|
|
228
|
-
"target": "app/components/
|
|
227
|
+
"path": "site-source/app/components/theme/Menu.vue",
|
|
228
|
+
"target": "app/components/theme/Menu.vue"
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
|
-
"path": "site-source/app/components/ui/
|
|
232
|
-
"target": "app/components/ui/
|
|
231
|
+
"path": "site-source/app/components/ui/Button.vue",
|
|
232
|
+
"target": "app/components/ui/Button.vue"
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
|
-
"path": "site-source/app/components/ui/
|
|
236
|
-
"target": "app/components/ui/
|
|
235
|
+
"path": "site-source/app/components/ui/GradientBorder.vue",
|
|
236
|
+
"target": "app/components/ui/GradientBorder.vue"
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
|
-
"path": "site-source/app/components/ui/
|
|
240
|
-
"target": "app/components/ui/
|
|
239
|
+
"path": "site-source/app/components/ui/Modal.vue",
|
|
240
|
+
"target": "app/components/ui/Modal.vue"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
|
-
"path": "site-source/app/components/ui/
|
|
244
|
-
"target": "app/components/ui/
|
|
243
|
+
"path": "site-source/app/components/ui/Popover.vue",
|
|
244
|
+
"target": "app/components/ui/Popover.vue"
|
|
245
245
|
},
|
|
246
246
|
{
|
|
247
|
-
"path": "site-source/app/components/ui/
|
|
248
|
-
"target": "app/components/ui/
|
|
247
|
+
"path": "site-source/app/components/ui/Text.vue",
|
|
248
|
+
"target": "app/components/ui/Text.vue"
|
|
249
249
|
},
|
|
250
250
|
{
|
|
251
|
-
"path": "site-source/app/components/
|
|
252
|
-
"target": "app/components/
|
|
251
|
+
"path": "site-source/app/components/checkout/Status.vue",
|
|
252
|
+
"target": "app/components/checkout/Status.vue"
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
|
-
"path": "site-source/app/components/
|
|
256
|
-
"target": "app/components/
|
|
255
|
+
"path": "site-source/app/components/checkout/Summary.vue",
|
|
256
|
+
"target": "app/components/checkout/Summary.vue"
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
|
-
"path": "site-source/app/components/
|
|
260
|
-
"target": "app/components/
|
|
259
|
+
"path": "site-source/app/components/google/SignInButton.vue",
|
|
260
|
+
"target": "app/components/google/SignInButton.vue"
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
"path": "site-source/app/components/ui/InfiniteScroll.vue",
|
|
264
264
|
"target": "app/components/ui/InfiniteScroll.vue"
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
|
-
"path": "site-source/app/components/
|
|
268
|
-
"target": "app/components/
|
|
267
|
+
"path": "site-source/app/components/youtube/Embed.vue",
|
|
268
|
+
"target": "app/components/youtube/Embed.vue"
|
|
269
269
|
},
|
|
270
270
|
{
|
|
271
271
|
"path": "site-source/app/composables/useCheckout.ts",
|
|
@@ -364,8 +364,20 @@
|
|
|
364
364
|
"target": "app/theme/tooltip.ts"
|
|
365
365
|
},
|
|
366
366
|
{
|
|
367
|
-
"path": "site-source/app/types
|
|
368
|
-
"target": "app/types
|
|
367
|
+
"path": "site-source/app/consent/types.ts",
|
|
368
|
+
"target": "app/consent/types.ts"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"path": "site-source/app/content/types.ts",
|
|
372
|
+
"target": "app/content/types.ts"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"path": "site-source/app/navigation/types.ts",
|
|
376
|
+
"target": "app/navigation/types.ts"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"path": "site-source/app/site/config.ts",
|
|
380
|
+
"target": "app/site/config.ts"
|
|
369
381
|
}
|
|
370
382
|
],
|
|
371
383
|
"dependencies": [],
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
environment:
|
|
10
|
+
description: Deployment environment
|
|
11
|
+
required: true
|
|
12
|
+
type: environment
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: deploy-${{ github.repository }}-${{ inputs.environment || 'production' }}
|
|
19
|
+
cancel-in-progress: false
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
deploy:
|
|
23
|
+
uses: 1yhy/stackonward/.github/workflows/deploy.yml@{{stackonwardRef}}
|
|
24
|
+
with:
|
|
25
|
+
config: deploy.config.json
|
|
26
|
+
environment: ${{ inputs.environment || 'production' }}
|
|
27
|
+
revision: ${{ github.sha }}
|
|
28
|
+
stackonward-ref: "{{stackonwardRef}}"
|
|
29
|
+
variables: ${{ vars.STACKONWARD_DEPLOY_VARIABLES || '{}' }}
|
|
30
|
+
secrets:
|
|
31
|
+
DEPLOY_REGISTRY_HOST: ${{ secrets.DEPLOY_REGISTRY_HOST }}
|
|
32
|
+
DEPLOY_REGISTRY_USERNAME: ${{ secrets.DEPLOY_REGISTRY_USERNAME }}
|
|
33
|
+
DEPLOY_REGISTRY_PASSWORD: ${{ secrets.DEPLOY_REGISTRY_PASSWORD }}
|
|
34
|
+
DEPLOY_SSH_HOST: ${{ secrets.DEPLOY_SSH_HOST }}
|
|
35
|
+
DEPLOY_SSH_PORT: ${{ secrets.DEPLOY_SSH_PORT }}
|
|
36
|
+
DEPLOY_SSH_USER: ${{ secrets.DEPLOY_SSH_USER }}
|
|
37
|
+
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
38
|
+
DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
version: 52
|
|
2
|
+
|
|
3
|
+
imports:
|
|
4
|
+
- projectPath: platform/stackonward
|
|
5
|
+
revision: "{{stackonwardRef}}"
|
|
6
|
+
|
|
7
|
+
# 导入任务从当前构建解析完整提交和不可变制品身份,调用方只覆盖部署意图。
|
|
8
|
+
properties:
|
|
9
|
+
- name: stackonwardRef
|
|
10
|
+
value: "{{stackonwardRef}}"
|
|
11
|
+
- name: deployConfig
|
|
12
|
+
value: deploy.config.json
|
|
13
|
+
- name: deployEnvironment
|
|
14
|
+
value: production
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
map "$status:$uri" $site_cache_control {
|
|
2
|
+
default "no-store";
|
|
3
|
+
|
|
4
|
+
"~^[0-9]{3}:/api(?:/|$)" "private, no-store";
|
|
5
|
+
"~*^(?:200|206|304):/(?:service-worker|sw)\.js$" "no-cache, max-age=0, must-revalidate";
|
|
6
|
+
"~*^(?:200|206|304):/(?:asset-manifest|manifest)\.json$" "no-cache, max-age=0, must-revalidate";
|
|
7
|
+
"~*^(?:200|206|304):/manifest\.webmanifest$" "no-cache, max-age=0, must-revalidate";
|
|
8
|
+
"~*^(?:200|206|304):.*\.html?$" "no-cache, max-age=0, must-revalidate";
|
|
9
|
+
|
|
10
|
+
"~*^(?:200|206|304):/(?:_nuxt|_next/static)/" "public, max-age=31536000, immutable";
|
|
11
|
+
"~*^(?:200|206|304):/assets/.*[._-][A-Za-z0-9_-]{8,}\." "public, max-age=31536000, immutable";
|
|
12
|
+
"~*^(?:200|206|304):.*/[^/]*[._-][0-9a-f]{8,}(?:[._-][^/]*)?\.(?:avif|css|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|ogg|otf|pdf|png|svg|ttf|wav|webm|webmanifest|webp|woff2?|xml)$" "public, max-age=31536000, immutable";
|
|
13
|
+
|
|
14
|
+
"~*^(?:200|206|304):.*\.(?:avif|eot|gif|ico|jpe?g|mp3|mp4|ogg|otf|pdf|png|svg|ttf|wav|webm|webp|woff2?)$" "public, max-age=86400, stale-while-revalidate=604800";
|
|
15
|
+
"~*^(?:200|206|304):.*\.(?:css|js|json|map|mjs|txt|webmanifest|xml)$" "public, max-age=3600, must-revalidate";
|
|
16
|
+
"~^(?:200|304):" "no-cache, max-age=0, must-revalidate";
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
gzip on;
|
|
2
|
+
gzip_static on;
|
|
3
|
+
gzip_vary on;
|
|
4
|
+
gzip_proxied any;
|
|
5
|
+
gzip_comp_level 6;
|
|
6
|
+
gzip_min_length 1024;
|
|
7
|
+
gzip_types
|
|
8
|
+
application/atom+xml
|
|
9
|
+
application/javascript
|
|
10
|
+
application/json
|
|
11
|
+
application/ld+json
|
|
12
|
+
application/manifest+json
|
|
13
|
+
application/rss+xml
|
|
14
|
+
application/wasm
|
|
15
|
+
application/xhtml+xml
|
|
16
|
+
application/xml
|
|
17
|
+
image/svg+xml
|
|
18
|
+
text/css
|
|
19
|
+
text/csv
|
|
20
|
+
text/javascript
|
|
21
|
+
text/plain
|
|
22
|
+
text/vtt
|
|
23
|
+
text/xml;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
proxy_http_version 1.1;
|
|
2
|
+
proxy_set_header Connection "";
|
|
3
|
+
proxy_set_header Host $host;
|
|
4
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
5
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
6
|
+
proxy_set_header X-Forwarded-Host $host;
|
|
7
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
8
|
+
proxy_set_header X-Forwarded-Port $server_port;
|
|
9
|
+
|
|
10
|
+
proxy_connect_timeout 5s;
|
|
11
|
+
proxy_send_timeout 60s;
|
|
12
|
+
proxy_read_timeout 60s;
|
|
13
|
+
proxy_next_upstream error timeout invalid_header http_502 http_503 http_504;
|
|
14
|
+
proxy_next_upstream_timeout 10s;
|
|
15
|
+
proxy_next_upstream_tries 2;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
image: ${DEPLOY_IMAGE:?set DEPLOY_IMAGE to an immutable repository@sha256 digest}
|
|
4
|
+
pull_policy: always
|
|
5
|
+
init: true
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
environment:
|
|
8
|
+
APP_ENV: production
|
|
9
|
+
APP_HOST: 0.0.0.0
|
|
10
|
+
APP_PORT: 3100
|
|
11
|
+
ports:
|
|
12
|
+
- "${DEPLOY_BIND_ADDRESS:-127.0.0.1}:${DEPLOY_HOST_PORT:-3100}:3100"
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: [CMD, wget, -q, --spider, "http://127.0.0.1:3100/health/readiness"]
|
|
15
|
+
interval: 10s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 6
|
|
18
|
+
start_period: 10s
|
|
19
|
+
stop_grace_period: 45s
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
|
|
3
|
+
FROM node:22.22.1-alpine3.23@sha256:8094c002d08262dba12645a3b4a15cd6cd627d30bc782f53229a2ec13ee22a00 AS runtime
|
|
4
|
+
|
|
5
|
+
ENV NODE_ENV=production
|
|
6
|
+
ENV NITRO_HOST=0.0.0.0
|
|
7
|
+
ENV NITRO_PORT=3000
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
COPY --chown=node:node .output ./.output
|
|
12
|
+
|
|
13
|
+
USER node
|
|
14
|
+
|
|
15
|
+
EXPOSE 3000
|
|
16
|
+
|
|
17
|
+
CMD ["node", ".output/server/index.mjs"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 站点片段由主机 server 块加载;缓存映射必须先在 http 上下文加载。
|
|
2
|
+
root "{{root}}/current";
|
|
3
|
+
index index.html;
|
|
4
|
+
autoindex off;
|
|
5
|
+
etag on;
|
|
6
|
+
|
|
7
|
+
gzip on;
|
|
8
|
+
gzip_vary on;
|
|
9
|
+
gzip_proxied any;
|
|
10
|
+
gzip_comp_level 6;
|
|
11
|
+
gzip_min_length 1024;
|
|
12
|
+
gzip_types application/javascript application/json application/manifest+json application/wasm image/svg+xml text/css text/javascript text/plain text/xml;
|
|
13
|
+
|
|
14
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
15
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
16
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
17
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
18
|
+
|
|
19
|
+
location = /api {
|
|
20
|
+
add_header Cache-Control "private, no-store" always;
|
|
21
|
+
try_files $uri =404;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
location ^~ /api/ {
|
|
25
|
+
add_header Cache-Control "private, no-store" always;
|
|
26
|
+
try_files $uri =404;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
location ^~ /.well-known/ {
|
|
30
|
+
try_files $uri =404;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
location ~ /\.(?!well-known(?:/|$)) {
|
|
34
|
+
deny all;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
location ~* \.html?$ {
|
|
38
|
+
add_header Cache-Control "no-cache, max-age=0, must-revalidate" always;
|
|
39
|
+
try_files $uri =404;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
location ~* \.(?:css|js|mjs|json|map|txt|webmanifest|xml)$ {
|
|
43
|
+
try_files $uri =404;
|
|
44
|
+
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
location ~* \.(?:avif|eot|gif|ico|jpe?g|mp3|mp4|ogg|otf|pdf|png|svg|ttf|wav|webm|webp|woff2?)$ {
|
|
48
|
+
try_files $uri =404;
|
|
49
|
+
add_header Cache-Control "public, max-age=86400, stale-while-revalidate=604800" always;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
location / {
|
|
53
|
+
add_header Cache-Control "no-cache, max-age=0, must-revalidate" always;
|
|
54
|
+
try_files $uri $uri/ /index.html;
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM nginxinc/nginx-unprivileged:1.30.4-alpine@sha256:adf5042a17f4ecdd200c595fa9ffd1be37efb18f89a830bd1a00e4ab4d59d42c
|
|
2
|
+
|
|
3
|
+
ARG STATIC_OUTPUT=__required__
|
|
4
|
+
|
|
5
|
+
RUN test "${STATIC_OUTPUT}" != "__required__" \
|
|
6
|
+
|| { echo "STATIC_OUTPUT is required" >&2; exit 64; }
|
|
7
|
+
|
|
8
|
+
COPY --chown=101:0 deployments/stackonward/nginx/ /etc/nginx/policies/
|
|
9
|
+
COPY --chown=101:0 deployments/stackonward/profiles/static-nginx/nginx/default.conf.template /etc/nginx/templates/default.conf.template
|
|
10
|
+
COPY --chown=101:0 ${STATIC_OUTPUT}/ /usr/share/nginx/html/
|
|
11
|
+
|
|
12
|
+
RUN printf '%s\n' 'include /etc/nginx/policies/cache.conf;' > /etc/nginx/conf.d/00-cache.conf
|
|
13
|
+
|
|
14
|
+
USER 101
|
|
15
|
+
EXPOSE 8080
|
|
16
|
+
|
|
17
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
18
|
+
CMD wget --quiet --tries=1 --spider http://127.0.0.1:8080/ || exit 1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 8080 default_server;
|
|
3
|
+
listen [::]:8080 default_server;
|
|
4
|
+
server_name _;
|
|
5
|
+
|
|
6
|
+
root /usr/share/nginx/html;
|
|
7
|
+
index index.html;
|
|
8
|
+
charset utf-8;
|
|
9
|
+
|
|
10
|
+
include /etc/nginx/policies/compression.conf;
|
|
11
|
+
add_header Cache-Control $site_cache_control always;
|
|
12
|
+
|
|
13
|
+
location = /api {
|
|
14
|
+
return 404;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
location ^~ /api/ {
|
|
18
|
+
return 404;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
location ~* \.(?:avif|css|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|ogg|otf|pdf|png|svg|ttf|txt|wasm|wav|webmanifest|webm|webp|woff2?|xml|zip)$ {
|
|
22
|
+
try_files $uri =404;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
location / {
|
|
26
|
+
try_files $uri $uri/ /index.html;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "application",
|
|
4
|
+
"profile": "static-nginx",
|
|
5
|
+
"source": {
|
|
6
|
+
"verify": {
|
|
7
|
+
"executable": "pnpm",
|
|
8
|
+
"args": ["run", "check"]
|
|
9
|
+
},
|
|
10
|
+
"build": {
|
|
11
|
+
"executable": "pnpm",
|
|
12
|
+
"args": ["run", "build"]
|
|
13
|
+
},
|
|
14
|
+
"output": "dist"
|
|
15
|
+
},
|
|
16
|
+
"image": {
|
|
17
|
+
"dockerfile": "Dockerfile",
|
|
18
|
+
"context": ".",
|
|
19
|
+
"repository": "application",
|
|
20
|
+
"platform": "linux/amd64"
|
|
21
|
+
},
|
|
22
|
+
"environments": {
|
|
23
|
+
"production": {
|
|
24
|
+
"target": "compose-host",
|
|
25
|
+
"root": "/srv/application",
|
|
26
|
+
"compose": ["deployments/compose.yaml"],
|
|
27
|
+
"rollout": {
|
|
28
|
+
"strategy": "blue-green",
|
|
29
|
+
"ports": {
|
|
30
|
+
"blue": [
|
|
31
|
+
{
|
|
32
|
+
"service": "app",
|
|
33
|
+
"host": "127.0.0.1",
|
|
34
|
+
"targetPort": 3000,
|
|
35
|
+
"publishedPort": { "env": "DEPLOY_BLUE_PORT" },
|
|
36
|
+
"protocol": "tcp"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"green": [
|
|
40
|
+
{
|
|
41
|
+
"service": "app",
|
|
42
|
+
"host": "127.0.0.1",
|
|
43
|
+
"targetPort": 3000,
|
|
44
|
+
"publishedPort": { "env": "DEPLOY_GREEN_PORT" },
|
|
45
|
+
"protocol": "tcp"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"previewHealth": {
|
|
50
|
+
"blue": {
|
|
51
|
+
"url": { "env": "DEPLOY_BLUE_HEALTH_URL" },
|
|
52
|
+
"method": "GET",
|
|
53
|
+
"expectedStatus": 200,
|
|
54
|
+
"attempts": 12,
|
|
55
|
+
"intervalMs": 5000,
|
|
56
|
+
"timeoutMs": 5000
|
|
57
|
+
},
|
|
58
|
+
"green": {
|
|
59
|
+
"url": { "env": "DEPLOY_GREEN_HEALTH_URL" },
|
|
60
|
+
"method": "GET",
|
|
61
|
+
"expectedStatus": 200,
|
|
62
|
+
"attempts": 12,
|
|
63
|
+
"intervalMs": 5000,
|
|
64
|
+
"timeoutMs": 5000
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"traffic": {
|
|
68
|
+
"inspect": {
|
|
69
|
+
"executable": "/usr/local/bin/stackonward-nginx-traffic",
|
|
70
|
+
"args": ["inspect"],
|
|
71
|
+
"env": [
|
|
72
|
+
"DEPLOY_TRAFFIC_STATE",
|
|
73
|
+
"DEPLOY_NGINX_UPSTREAM",
|
|
74
|
+
"DEPLOY_BLUE_PORT",
|
|
75
|
+
"DEPLOY_GREEN_PORT"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"activate": {
|
|
79
|
+
"executable": "/usr/local/bin/stackonward-nginx-traffic",
|
|
80
|
+
"args": ["activate"],
|
|
81
|
+
"env": [
|
|
82
|
+
"DEPLOY_TRAFFIC_STATE",
|
|
83
|
+
"DEPLOY_NGINX_UPSTREAM",
|
|
84
|
+
"DEPLOY_BLUE_PORT",
|
|
85
|
+
"DEPLOY_GREEN_PORT"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"recover": {
|
|
89
|
+
"executable": "/usr/local/bin/stackonward-nginx-traffic",
|
|
90
|
+
"args": ["recover"],
|
|
91
|
+
"env": [
|
|
92
|
+
"DEPLOY_TRAFFIC_STATE",
|
|
93
|
+
"DEPLOY_NGINX_UPSTREAM",
|
|
94
|
+
"DEPLOY_BLUE_PORT",
|
|
95
|
+
"DEPLOY_GREEN_PORT"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"health": {
|
|
101
|
+
"url": "https://deployment.invalid/health",
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"expectedStatus": 200,
|
|
104
|
+
"attempts": 12,
|
|
105
|
+
"intervalMs": 5000,
|
|
106
|
+
"timeoutMs": 5000
|
|
107
|
+
},
|
|
108
|
+
"retention": {
|
|
109
|
+
"releases": 5,
|
|
110
|
+
"buildCacheMax": "10gb"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
state=${STACKONWARD_TRAFFIC_STATE:?STACKONWARD_TRAFFIC_STATE is required}
|
|
5
|
+
upstream=${STACKONWARD_NGINX_UPSTREAM:?STACKONWARD_NGINX_UPSTREAM is required}
|
|
6
|
+
blue_port=${STACKONWARD_BLUE_PORT:?STACKONWARD_BLUE_PORT is required}
|
|
7
|
+
green_port=${STACKONWARD_GREEN_PORT:?STACKONWARD_GREEN_PORT is required}
|
|
8
|
+
|
|
9
|
+
read_state() {
|
|
10
|
+
active=blue
|
|
11
|
+
release=
|
|
12
|
+
if [ -f "$state" ]; then
|
|
13
|
+
active=$(awk -F= '$1 == "active" { print $2 }' "$state")
|
|
14
|
+
release=$(awk -F= '$1 == "release" { print $2 }' "$state")
|
|
15
|
+
fi
|
|
16
|
+
case "$active" in
|
|
17
|
+
blue|green) ;;
|
|
18
|
+
*) echo "invalid traffic state" >&2; exit 2 ;;
|
|
19
|
+
esac
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
emit() {
|
|
23
|
+
phase=$1
|
|
24
|
+
candidate=${2-}
|
|
25
|
+
printf '{"schemaVersion":1,"status":"succeeded","phase":"%s","active":"%s"' "$phase" "$active"
|
|
26
|
+
if [ -n "$candidate" ]; then
|
|
27
|
+
printf ',"candidate":"%s"' "$candidate"
|
|
28
|
+
fi
|
|
29
|
+
printf '}\n'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
write_route() {
|
|
33
|
+
slot=$1
|
|
34
|
+
case "$slot" in
|
|
35
|
+
blue) port=$blue_port ;;
|
|
36
|
+
green) port=$green_port ;;
|
|
37
|
+
*) echo "invalid traffic slot" >&2; exit 2 ;;
|
|
38
|
+
esac
|
|
39
|
+
directory=$(dirname "$upstream")
|
|
40
|
+
mkdir -p "$directory"
|
|
41
|
+
temporary="$upstream.$$"
|
|
42
|
+
backup="$upstream.$$.bak"
|
|
43
|
+
had_upstream=0
|
|
44
|
+
if [ -f "$upstream" ]; then
|
|
45
|
+
cp "$upstream" "$backup"
|
|
46
|
+
had_upstream=1
|
|
47
|
+
fi
|
|
48
|
+
printf 'server 127.0.0.1:%s;\n' "$port" > "$temporary"
|
|
49
|
+
mv -f "$temporary" "$upstream"
|
|
50
|
+
if ! nginx -t >/dev/null 2>&1 || ! nginx -s reload >/dev/null 2>&1; then
|
|
51
|
+
rm -f "$temporary"
|
|
52
|
+
if [ "$had_upstream" -eq 1 ]; then
|
|
53
|
+
mv -f "$backup" "$upstream"
|
|
54
|
+
nginx -s reload >/dev/null 2>&1 || true
|
|
55
|
+
else
|
|
56
|
+
rm -f "$upstream"
|
|
57
|
+
fi
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
rm -f "$backup"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
write_state() {
|
|
64
|
+
temporary="$state.$$"
|
|
65
|
+
directory=$(dirname "$state")
|
|
66
|
+
mkdir -p "$directory"
|
|
67
|
+
umask 077
|
|
68
|
+
printf 'active=%s\nrelease=%s\n' "$active" "$release" > "$temporary"
|
|
69
|
+
mv -f "$temporary" "$state"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
read_state
|
|
73
|
+
case "${1-}" in
|
|
74
|
+
inspect)
|
|
75
|
+
emit inspect
|
|
76
|
+
;;
|
|
77
|
+
activate|recover)
|
|
78
|
+
candidate_slot=${STACKONWARD_CANDIDATE_SLOT:?STACKONWARD_CANDIDATE_SLOT is required}
|
|
79
|
+
candidate_release=${STACKONWARD_CANDIDATE_RELEASE:?STACKONWARD_CANDIDATE_RELEASE is required}
|
|
80
|
+
previous_active=$active
|
|
81
|
+
previous_release=$release
|
|
82
|
+
active=$candidate_slot
|
|
83
|
+
release=$candidate_release
|
|
84
|
+
if ! write_route "$active"; then
|
|
85
|
+
active=$previous_active
|
|
86
|
+
release=$previous_release
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
write_state
|
|
90
|
+
emit "$1" "$release"
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
echo "usage: stackonward-nginx-traffic inspect|activate|recover" >&2
|
|
94
|
+
exit 2
|
|
95
|
+
;;
|
|
96
|
+
esac
|