@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.
Files changed (111) hide show
  1. package/README.md +248 -0
  2. package/dist/index.js +14124 -635
  3. package/dist/index.js.map +1 -1
  4. package/dist/resources/components/nuxt/site-source/app/app.vue +4 -2
  5. package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +50 -50
  6. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationHeader.vue → app/Header.vue} +3 -3
  7. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationShell.vue → app/Shell.vue} +2 -2
  8. package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationSidebar.vue → app/Sidebar.vue} +2 -2
  9. package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutStatus.vue → checkout/Status.vue} +8 -3
  10. package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutSummary.vue → checkout/Summary.vue} +11 -11
  11. package/dist/resources/components/nuxt/site-source/app/components/{site/CookieConsent.vue → consent/Banner.vue} +3 -3
  12. package/dist/resources/components/nuxt/site-source/app/components/{site/ContentArticlePage.vue → content/Article.vue} +3 -3
  13. package/dist/resources/components/nuxt/site-source/app/components/{site/ContentListPage.vue → content/List.vue} +2 -2
  14. package/dist/resources/components/nuxt/site-source/app/components/{ui/GoogleSignInButton.vue → google/SignInButton.vue} +3 -3
  15. package/dist/resources/components/nuxt/site-source/app/components/{site/LocaleSwitcher.vue → locale/Menu.vue} +2 -2
  16. package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingFooter.vue → Footer.vue} +8 -8
  17. package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingHeader.vue → Header.vue} +27 -27
  18. package/dist/resources/components/nuxt/site-source/app/components/site/Shell.vue +29 -0
  19. package/dist/resources/components/nuxt/site-source/app/components/{site/ThemeSwitcher.vue → theme/Menu.vue} +2 -2
  20. package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseButton.vue → Button.vue} +1 -8
  21. package/dist/resources/components/nuxt/site-source/app/components/ui/InfiniteScroll.vue +2 -2
  22. package/dist/resources/components/nuxt/site-source/app/components/{ui/YoutubeEmbed.vue → youtube/Embed.vue} +10 -2
  23. package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +8 -10
  24. package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +9 -14
  25. package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +3 -1
  26. package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +19 -22
  27. package/dist/resources/components/nuxt/site-source/app/consent/types.ts +59 -0
  28. package/dist/resources/components/nuxt/site-source/app/content/types.ts +17 -0
  29. package/dist/resources/components/nuxt/site-source/app/layouts/app.vue +2 -2
  30. package/dist/resources/components/nuxt/site-source/app/layouts/default.vue +2 -2
  31. package/dist/resources/components/nuxt/site-source/app/navigation/types.ts +68 -0
  32. package/dist/resources/components/nuxt/site-source/app/plugins/consent.ts +2 -2
  33. package/dist/resources/components/nuxt/site-source/app/site/config.ts +35 -0
  34. package/dist/resources/components/registry.json +63 -51
  35. package/dist/resources/deploy/ci/github/caller.yml +38 -0
  36. package/dist/resources/deploy/ci/onedev/import.yml +14 -0
  37. package/dist/resources/deploy/nginx/cache.conf +17 -0
  38. package/dist/resources/deploy/nginx/compression.conf +23 -0
  39. package/dist/resources/deploy/nginx/proxy.conf +15 -0
  40. package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
  41. package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
  42. package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
  43. package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
  44. package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
  45. package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
  46. package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
  47. package/dist/resources/registry.json +30 -30
  48. package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
  49. package/dist/resources/templates/base/AGENTS.md +15 -0
  50. package/dist/resources/templates/base/CLAUDE.md +15 -0
  51. package/dist/resources/templates/go-backend/.dockerignore +25 -3
  52. package/dist/resources/templates/go-backend/deploy.config.json +26 -0
  53. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
  54. package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
  55. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
  56. package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
  57. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
  58. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
  59. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
  60. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
  61. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
  62. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
  63. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
  64. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
  65. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
  66. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
  67. package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
  68. package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
  69. package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
  70. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
  71. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
  72. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
  73. package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
  74. package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
  75. package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
  76. package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
  77. package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
  78. package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
  79. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
  80. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
  81. package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
  82. package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
  83. package/dist/resources/templates/nuxt/.dockerignore +19 -0
  84. package/dist/resources/templates/nuxt/README.md +3 -3
  85. package/dist/resources/templates/nuxt/app/app.config.ts +1 -1
  86. package/dist/resources/templates/nuxt/app/i18n/locales/en.json +1 -1
  87. package/dist/resources/templates/nuxt/app/pages/app/index.vue +1 -1
  88. package/dist/resources/templates/nuxt/app/pages/authors/[slug].vue +2 -2
  89. package/dist/resources/templates/nuxt/app/pages/blog/[slug].vue +4 -3
  90. package/dist/resources/templates/nuxt/app/pages/blog/category/[category].vue +2 -2
  91. package/dist/resources/templates/nuxt/app/pages/blog/index.vue +2 -2
  92. package/dist/resources/templates/nuxt/app/pages/legal/cookies.vue +3 -3
  93. package/dist/resources/templates/nuxt/app/pages/legal/privacy.vue +2 -2
  94. package/dist/resources/templates/nuxt/app/pages/legal/terms.vue +2 -2
  95. package/dist/resources/templates/nuxt/app/pages/ui/index.vue +45 -45
  96. package/dist/resources/templates/nuxt/deploy.config.json +27 -0
  97. package/dist/resources/templates/nuxt/nuxt.config.ts +4 -10
  98. package/dist/resources/templates/nuxt/package.json +8 -8
  99. package/dist/resources/templates/nuxt/test/e2e/rendering.test.ts +1 -0
  100. package/dist/resources/templates/nuxt/test/unit/consent.test.ts +4 -7
  101. package/package.json +5 -3
  102. package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +0 -33
  103. package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +0 -172
  104. /package/dist/resources/components/nuxt/site-source/app/components/{site/ConsentSettingsButton.vue → consent/Settings.vue} +0 -0
  105. /package/dist/resources/components/nuxt/site-source/app/components/{site/StaticPage.vue → content/Document.vue} +0 -0
  106. /package/dist/resources/components/nuxt/site-source/app/components/{site/ToastViewport.vue → feedback/Toast.vue} +0 -0
  107. /package/dist/resources/components/nuxt/site-source/app/components/site/{SiteBrand.vue → Brand.vue} +0 -0
  108. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseGradientBorder.vue → GradientBorder.vue} +0 -0
  109. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseModal.vue → Modal.vue} +0 -0
  110. /package/dist/resources/components/nuxt/site-source/app/components/ui/{BasePopover.vue → Popover.vue} +0 -0
  111. /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: app shell, marketing shell, content shell, UI primitives, composables, plugins, theme tokens, and CSS.",
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/site/ApplicationHeader.vue",
168
- "target": "app/components/site/ApplicationHeader.vue"
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/site/ApplicationShell.vue",
172
- "target": "app/components/site/ApplicationShell.vue"
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/site/ApplicationSidebar.vue",
176
- "target": "app/components/site/ApplicationSidebar.vue"
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/site/ContentArticlePage.vue",
184
- "target": "app/components/site/ContentArticlePage.vue"
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/site/ContentListPage.vue",
188
- "target": "app/components/site/ContentListPage.vue"
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/site/ConsentSettingsButton.vue",
192
- "target": "app/components/site/ConsentSettingsButton.vue"
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/site/CookieConsent.vue",
196
- "target": "app/components/site/CookieConsent.vue"
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/site/LocaleSwitcher.vue",
200
- "target": "app/components/site/LocaleSwitcher.vue"
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/MarketingFooter.vue",
204
- "target": "app/components/site/MarketingFooter.vue"
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/MarketingHeader.vue",
208
- "target": "app/components/site/MarketingHeader.vue"
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/MarketingShell.vue",
212
- "target": "app/components/site/MarketingShell.vue"
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/SiteBrand.vue",
216
- "target": "app/components/site/SiteBrand.vue"
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/site/StaticPage.vue",
220
- "target": "app/components/site/StaticPage.vue"
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/site/ToastViewport.vue",
224
- "target": "app/components/site/ToastViewport.vue"
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/site/ThemeSwitcher.vue",
228
- "target": "app/components/site/ThemeSwitcher.vue"
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/BaseButton.vue",
232
- "target": "app/components/ui/BaseButton.vue"
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/BaseGradientBorder.vue",
236
- "target": "app/components/ui/BaseGradientBorder.vue"
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/BaseModal.vue",
240
- "target": "app/components/ui/BaseModal.vue"
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/BasePopover.vue",
244
- "target": "app/components/ui/BasePopover.vue"
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/BaseText.vue",
248
- "target": "app/components/ui/BaseText.vue"
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/ui/CheckoutStatus.vue",
252
- "target": "app/components/ui/CheckoutStatus.vue"
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/ui/CheckoutSummary.vue",
256
- "target": "app/components/ui/CheckoutSummary.vue"
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/ui/GoogleSignInButton.vue",
260
- "target": "app/components/ui/GoogleSignInButton.vue"
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/ui/YoutubeEmbed.vue",
268
- "target": "app/components/ui/YoutubeEmbed.vue"
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/site-shell.ts",
368
- "target": "app/types/site-shell.ts"
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