@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.
Files changed (55) hide show
  1. package/README.md +255 -0
  2. package/dist/index.js +14335 -603
  3. package/dist/index.js.map +1 -1
  4. package/dist/resources/deploy/ci/github/caller.yml +41 -0
  5. package/dist/resources/deploy/ci/onedev/import.yml +40 -0
  6. package/dist/resources/deploy/nginx/cache.conf +17 -0
  7. package/dist/resources/deploy/nginx/compression.conf +23 -0
  8. package/dist/resources/deploy/nginx/proxy.conf +15 -0
  9. package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
  10. package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
  11. package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
  12. package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
  13. package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
  14. package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
  15. package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
  16. package/dist/resources/registry.json +30 -30
  17. package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
  18. package/dist/resources/templates/base/AGENTS.md +15 -0
  19. package/dist/resources/templates/base/CLAUDE.md +15 -0
  20. package/dist/resources/templates/go-backend/.dockerignore +25 -3
  21. package/dist/resources/templates/go-backend/deploy.config.json +26 -0
  22. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
  23. package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
  24. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
  25. package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
  26. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
  27. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
  28. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
  29. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
  30. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
  31. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
  32. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
  33. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
  34. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
  35. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
  36. package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
  37. package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
  38. package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
  39. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
  40. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
  41. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
  42. package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
  43. package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
  44. package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
  45. package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
  46. package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
  47. package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
  48. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
  49. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
  50. package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
  51. package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
  52. package/dist/resources/templates/nuxt/.dockerignore +19 -0
  53. package/dist/resources/templates/nuxt/deploy.config.json +27 -0
  54. package/dist/resources/templates/nuxt/package.json +8 -8
  55. 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