@stackonward/cli 1.4.0 → 1.5.1
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 +255 -0
- package/dist/index.js +14335 -603
- package/dist/index.js.map +1 -1
- package/dist/resources/deploy/ci/github/caller.yml +41 -0
- package/dist/resources/deploy/ci/onedev/import.yml +40 -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/deploy.config.json +27 -0
- package/dist/resources/templates/nuxt/package.json +8 -8
- package/package.json +5 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- test
|
|
7
|
+
- stage
|
|
8
|
+
- staging
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
environment:
|
|
12
|
+
description: Deployment environment
|
|
13
|
+
required: true
|
|
14
|
+
type: environment
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: deploy-${{ github.repository }}-${{ github.event_name == 'workflow_dispatch' && inputs.environment || github.ref_name }}
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
deploy:
|
|
25
|
+
uses: 1yhy/stackonward/.github/workflows/deploy.yml@{{stackonwardRef}}
|
|
26
|
+
with:
|
|
27
|
+
config: deploy.config.json
|
|
28
|
+
environment: ${{ github.event_name == 'workflow_dispatch' && inputs.environment || (github.ref_name == 'staging' && 'stage' || github.ref_name) }}
|
|
29
|
+
revision: ${{ github.sha }}
|
|
30
|
+
stackonward-ref: "{{stackonwardRef}}"
|
|
31
|
+
# STACKONWARD_DEPLOY_VARIABLES 是按环境名分包的 JSON,不是扁平键。
|
|
32
|
+
variables: ${{ vars.STACKONWARD_DEPLOY_VARIABLES || '{}' }}
|
|
33
|
+
secrets:
|
|
34
|
+
DEPLOY_REGISTRY_HOST: ${{ secrets.DEPLOY_REGISTRY_HOST }}
|
|
35
|
+
DEPLOY_REGISTRY_USERNAME: ${{ secrets.DEPLOY_REGISTRY_USERNAME }}
|
|
36
|
+
DEPLOY_REGISTRY_PASSWORD: ${{ secrets.DEPLOY_REGISTRY_PASSWORD }}
|
|
37
|
+
DEPLOY_SSH_HOST: ${{ secrets.DEPLOY_SSH_HOST }}
|
|
38
|
+
DEPLOY_SSH_PORT: ${{ secrets.DEPLOY_SSH_PORT }}
|
|
39
|
+
DEPLOY_SSH_USER: ${{ secrets.DEPLOY_SSH_USER }}
|
|
40
|
+
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
41
|
+
DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
|
15
|
+
- name: deployRegistryNamespace
|
|
16
|
+
value: unset
|
|
17
|
+
- name: deployVariables
|
|
18
|
+
value: "{}"
|
|
19
|
+
|
|
20
|
+
# 构建页播放键只列出本仓库 jobs。本地 Build 依赖导入的 Release。
|
|
21
|
+
jobs:
|
|
22
|
+
- name: Build
|
|
23
|
+
steps:
|
|
24
|
+
- type: CommandStep
|
|
25
|
+
name: complete
|
|
26
|
+
runInContainer: false
|
|
27
|
+
interpreter:
|
|
28
|
+
type: PosixInterpreter
|
|
29
|
+
shell: bash
|
|
30
|
+
commands: |
|
|
31
|
+
echo "ok"
|
|
32
|
+
useTTY: false
|
|
33
|
+
condition: SUCCESSFUL
|
|
34
|
+
optional: false
|
|
35
|
+
jobDependencies:
|
|
36
|
+
- jobName: StackOnward Release
|
|
37
|
+
requireSuccessful: true
|
|
38
|
+
artifacts: deployments/ci/** static-files/**
|
|
39
|
+
retryCondition: never
|
|
40
|
+
timeout: 1800
|
|
@@ -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
|