@wdyy/skills 0.1.11 → 0.1.12
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/.well-known/skills/index.json +2 -2
- package/.well-known/skills/wdyy-deployment-standard/SKILL.md +31 -40
- package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +27 -55
- package/.well-known/skills/wdyy-deployment-standard/scripts/generate-deployment-files.mjs +9 -5
- package/.well-known/skills/wdyy-deployment-standard/scripts/generate-deployment-files.test.mjs +14 -2
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +60 -109
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +151 -277
- package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +182 -576
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.yml +35 -0
- package/.well-known/skills/wdyy-deployment-standard/templates/env.example.template +0 -5
- package/.well-known/skills/wdyy-deployment-standard/templates/frontend-container.conf.template +2 -2
- package/.well-known/skills/wdyy-logging-standard/SKILL.md +2 -2
- package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +1 -1
- package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +2 -2
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +1 -1
- package/README.md +4 -2
- package/lib/wdyy-cli.js +3 -2
- package/package.json +1 -1
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +0 -70
- package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +0 -25
|
@@ -5,6 +5,9 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -
|
|
|
5
5
|
SCRIPT_PATH="$SCRIPT_DIR/$(basename -- "${BASH_SOURCE[0]}")"
|
|
6
6
|
CHECKSUM_TOOL=
|
|
7
7
|
BUILD_LOCK=
|
|
8
|
+
DEPLOY_ROOT=
|
|
9
|
+
RELEASE_VERSION=
|
|
10
|
+
OPERATION=
|
|
8
11
|
|
|
9
12
|
fail() {
|
|
10
13
|
echo "$*" >&2
|
|
@@ -12,7 +15,8 @@ fail() {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
usage() {
|
|
15
|
-
echo "usage: ./deploy.sh build
|
|
18
|
+
echo "usage: ./deploy.sh build # local project root" >&2
|
|
19
|
+
echo " ./deploy.sh # server project root menu" >&2
|
|
16
20
|
exit 2
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -31,31 +35,29 @@ assert_port() {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
reset_configuration() {
|
|
34
|
-
unset PROJECT_NAME
|
|
38
|
+
unset PROJECT_NAME DOCKER_BIND_IP
|
|
35
39
|
unset FRONTEND_URL FRONTEND_PORT BACKEND_URL BACKEND_PORT
|
|
36
|
-
unset FRONTEND_BLUE_PORT FRONTEND_GREEN_PORT BACKEND_BLUE_PORT BACKEND_GREEN_PORT
|
|
37
40
|
unset FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE
|
|
38
41
|
unset FRONTEND_DOCKERFILE BACKEND_DOCKERFILE DOCKERIGNORE_FILE
|
|
39
|
-
unset DEPLOY_CONFIG_DIR
|
|
40
|
-
unset COMPOSE_SOURCE NGINX_SOURCE
|
|
42
|
+
unset DEPLOY_CONFIG_DIR COMPOSE_SOURCE
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
is_deployment_key() {
|
|
44
46
|
case "$1" in
|
|
45
|
-
FRONTEND_URL|FRONTEND_PORT|BACKEND_URL|BACKEND_PORT|\
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
FRONTEND_BASE_IMAGE|BACKEND_BASE_IMAGE|\
|
|
49
|
-
FRONTEND_DOCKERFILE|BACKEND_DOCKERFILE|DOCKERIGNORE_FILE|\
|
|
50
|
-
DEPLOY_CONFIG_DIR|COMPOSE_SOURCE|NGINX_SOURCE)
|
|
47
|
+
FRONTEND_URL|FRONTEND_PORT|BACKEND_URL|BACKEND_PORT|PROJECT_NAME|DOCKER_BIND_IP|\
|
|
48
|
+
FRONTEND_BASE_IMAGE|BACKEND_BASE_IMAGE|FRONTEND_DOCKERFILE|BACKEND_DOCKERFILE|\
|
|
49
|
+
DOCKERIGNORE_FILE|DEPLOY_CONFIG_DIR|COMPOSE_SOURCE)
|
|
51
50
|
return 0 ;;
|
|
52
51
|
*) return 1 ;;
|
|
53
52
|
esac
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
is_removed_configuration_key() {
|
|
57
56
|
case "$1" in
|
|
58
|
-
|
|
57
|
+
PROJECT_HTTP_PORT|FRONTEND_BLUE_PORT|FRONTEND_GREEN_PORT|BACKEND_BLUE_PORT|BACKEND_GREEN_PORT|\
|
|
58
|
+
SERVER_PROJECTS_ROOT|NGINX_SOURCE|LOG_DIR|HOST_LOG_DIR|FRONTEND_CONTAINER_PORT|\
|
|
59
|
+
BACKEND_CONTAINER_PORT|BACKEND_LISTEN_HOST|DATABASE_MIGRATION_MODE|MIGRATIONS_SOURCE|\
|
|
60
|
+
MIGRATION_RUNNER_SOURCE|*_HEALTH_URL|*_VERSION_URL)
|
|
59
61
|
return 0 ;;
|
|
60
62
|
*) return 1 ;;
|
|
61
63
|
esac
|
|
@@ -63,16 +65,12 @@ is_legacy_configuration_key() {
|
|
|
63
65
|
|
|
64
66
|
validate_env_permissions() {
|
|
65
67
|
local file="$1" mode
|
|
66
|
-
if stat -c '%a' "$file" >/dev/null 2>&1; then
|
|
67
|
-
mode="$(stat -c '%a' "$file")"
|
|
68
|
-
else
|
|
69
|
-
mode="$(stat -f '%Lp' "$file")"
|
|
70
|
-
fi
|
|
68
|
+
if stat -c '%a' "$file" >/dev/null 2>&1; then mode="$(stat -c '%a' "$file")"; else mode="$(stat -f '%Lp' "$file")"; fi
|
|
71
69
|
[[ "$mode" == 400 || "$mode" == 600 ]] || fail "root .env permissions must be 400 or 600; got $mode"
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
load_dotenv() {
|
|
75
|
-
local file="$1" raw line key value first last
|
|
73
|
+
local file="$1" raw line key value first last loaded_key
|
|
76
74
|
local -a loaded_keys=()
|
|
77
75
|
[[ -r "$file" ]] || fail "required root environment file is missing: $file"
|
|
78
76
|
validate_env_permissions "$file"
|
|
@@ -80,56 +78,31 @@ load_dotenv() {
|
|
|
80
78
|
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
|
81
79
|
line="${raw%$'\r'}"
|
|
82
80
|
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
83
|
-
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] ||
|
|
84
|
-
|
|
85
|
-
key
|
|
86
|
-
|
|
87
|
-
[[ "$key" != LOG_DIR && "$key" != HOST_LOG_DIR ]] || fail "root .env must not configure log directories"
|
|
88
|
-
! is_legacy_configuration_key "$key" || fail "legacy duplicate environment key is forbidden: $key"
|
|
89
|
-
for loaded_key in "${loaded_keys[@]:-}"; do
|
|
90
|
-
[[ "$loaded_key" != "$key" ]] || fail "duplicate .env key: $key"
|
|
91
|
-
done
|
|
81
|
+
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || fail "invalid .env line; expected KEY=VALUE"
|
|
82
|
+
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"
|
|
83
|
+
! is_removed_configuration_key "$key" || fail "removed deployment configuration is forbidden: $key"
|
|
84
|
+
for loaded_key in "${loaded_keys[@]:-}"; do [[ "$loaded_key" != "$key" ]] || fail "duplicate .env key: $key"; done
|
|
92
85
|
loaded_keys+=("$key")
|
|
93
86
|
if [[ ${#value} -ge 2 ]]; then
|
|
94
|
-
first="${value:0:1}"
|
|
95
|
-
last="${value: -1}"
|
|
87
|
+
first="${value:0:1}"; last="${value: -1}"
|
|
96
88
|
if [[ "$first" == "'" || "$first" == '"' ]]; then
|
|
97
89
|
[[ "$last" == "$first" ]] || fail "unclosed quoted .env value: $key"
|
|
98
90
|
value="${value:1:${#value}-2}"
|
|
99
91
|
fi
|
|
100
92
|
fi
|
|
101
|
-
if is_deployment_key "$key"; then
|
|
102
|
-
printf -v "$key" '%s' "$value"
|
|
103
|
-
export "$key"
|
|
104
|
-
fi
|
|
93
|
+
if is_deployment_key "$key"; then printf -v "$key" '%s' "$value"; export "$key"; fi
|
|
105
94
|
done < "$file"
|
|
106
95
|
}
|
|
107
96
|
|
|
108
97
|
validate_configuration() {
|
|
109
|
-
local name
|
|
110
|
-
local -a required=(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
[[ -n "${!name:-}" ]] || fail "$name is required in root .env"
|
|
118
|
-
done
|
|
119
|
-
[[ "$PROJECT_NAME" =~ ^[a-z][a-z0-9_]*$ ]] ||
|
|
120
|
-
fail "PROJECT_NAME must match ^[a-z][a-z0-9_]*$"
|
|
121
|
-
[[ "$DOCKER_BIND_IP" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "DOCKER_BIND_IP contains unsafe characters"
|
|
122
|
-
[[ "$FRONTEND_URL" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "FRONTEND_URL contains unsafe characters"
|
|
123
|
-
[[ "$BACKEND_URL" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "BACKEND_URL contains unsafe characters"
|
|
124
|
-
for name in PROJECT_HTTP_PORT FRONTEND_PORT BACKEND_PORT \
|
|
125
|
-
FRONTEND_BLUE_PORT FRONTEND_GREEN_PORT BACKEND_BLUE_PORT BACKEND_GREEN_PORT; do
|
|
126
|
-
assert_port "$name" "${!name}"
|
|
127
|
-
done
|
|
128
|
-
for name in PROJECT_HTTP_PORT FRONTEND_BLUE_PORT FRONTEND_GREEN_PORT BACKEND_BLUE_PORT BACKEND_GREEN_PORT; do
|
|
129
|
-
port="${!name}"
|
|
130
|
-
[[ "$seen" != *" $port "* ]] || fail "host ports must be unique; duplicate value: $port"
|
|
131
|
-
seen+="$port "
|
|
132
|
-
done
|
|
98
|
+
local name
|
|
99
|
+
local -a required=(FRONTEND_URL FRONTEND_PORT BACKEND_URL BACKEND_PORT PROJECT_NAME DOCKER_BIND_IP FRONTEND_BASE_IMAGE BACKEND_BASE_IMAGE)
|
|
100
|
+
for name in "${required[@]}"; do [[ -n "${!name:-}" ]] || fail "$name is required in root .env"; done
|
|
101
|
+
[[ "$PROJECT_NAME" =~ ^[a-z][a-z0-9_]*$ ]] || fail "PROJECT_NAME must match ^[a-z][a-z0-9_]*$"
|
|
102
|
+
for name in DOCKER_BIND_IP FRONTEND_URL BACKEND_URL; do [[ "${!name}" =~ ^[A-Za-z0-9:.%-]+$ ]] || fail "$name contains unsafe characters"; done
|
|
103
|
+
assert_port FRONTEND_PORT "$FRONTEND_PORT"
|
|
104
|
+
assert_port BACKEND_PORT "$BACKEND_PORT"
|
|
105
|
+
[[ "$FRONTEND_PORT" != "$BACKEND_PORT" ]] || fail "FRONTEND_PORT and BACKEND_PORT must be different"
|
|
133
106
|
}
|
|
134
107
|
|
|
135
108
|
probe_host() {
|
|
@@ -149,615 +122,248 @@ endpoint_url() {
|
|
|
149
122
|
|
|
150
123
|
resolve_project_path() {
|
|
151
124
|
local project_root="$1" candidate="$2"
|
|
152
|
-
if [[ "$candidate" = /* ]]; then
|
|
153
|
-
printf '%s\n' "$candidate"
|
|
154
|
-
else
|
|
155
|
-
printf '%s/%s\n' "$project_root" "$candidate"
|
|
156
|
-
fi
|
|
125
|
+
if [[ "$candidate" = /* ]]; then printf '%s\n' "$candidate"; else printf '%s/%s\n' "$project_root" "$candidate"; fi
|
|
157
126
|
}
|
|
158
127
|
|
|
159
128
|
select_checksum_tool() {
|
|
160
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
CHECKSUM_TOOL=shasum
|
|
164
|
-
else
|
|
165
|
-
fail "sha256sum or shasum is required"
|
|
129
|
+
if command -v sha256sum >/dev/null 2>&1; then CHECKSUM_TOOL=sha256sum
|
|
130
|
+
elif command -v shasum >/dev/null 2>&1; then CHECKSUM_TOOL=shasum
|
|
131
|
+
else fail "sha256sum or shasum is required"
|
|
166
132
|
fi
|
|
167
133
|
}
|
|
168
134
|
|
|
169
135
|
checksum_value() {
|
|
170
|
-
|
|
171
|
-
if [[ "$CHECKSUM_TOOL" == sha256sum ]]; then
|
|
172
|
-
sha256sum "$file" | awk '{print $1}'
|
|
173
|
-
else
|
|
174
|
-
shasum -a 256 "$file" | awk '{print $1}'
|
|
175
|
-
fi
|
|
136
|
+
if [[ "$CHECKSUM_TOOL" == sha256sum ]]; then sha256sum "$1" | awk '{print $1}'; else shasum -a 256 "$1" | awk '{print $1}'; fi
|
|
176
137
|
}
|
|
177
138
|
|
|
178
139
|
release_ledger() {
|
|
179
140
|
node --input-type=module - "$@" <<'NODE'
|
|
180
141
|
import fs from 'node:fs';
|
|
181
|
-
|
|
182
142
|
const [mode, agentsPath, value, timestamp, outputPath] = process.argv.slice(2);
|
|
183
143
|
const heading = '## 发布记录';
|
|
184
144
|
const header = '| 版本号 | 构建时间 |';
|
|
185
145
|
const separator = '|---|---|';
|
|
186
|
-
const versionPattern = /^(\d{4})(\d{2})(\d{2})-(\d{3})$/;
|
|
187
146
|
const rowPattern = /^\| (\d{8}-\d{3}) \| (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{2}:\d{2}) \|$/;
|
|
188
|
-
|
|
189
147
|
function fail(message) { throw new Error(message); }
|
|
190
|
-
function validDate(text) {
|
|
191
|
-
const year = Number(text.slice(0, 4));
|
|
192
|
-
const month = Number(text.slice(4, 6));
|
|
193
|
-
const day = Number(text.slice(6, 8));
|
|
194
|
-
const date = new Date(Date.UTC(year, month - 1, day));
|
|
195
|
-
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
196
|
-
}
|
|
148
|
+
function validDate(text) { const y=Number(text.slice(0,4)),m=Number(text.slice(4,6)),d=Number(text.slice(6,8)),date=new Date(Date.UTC(y,m-1,d)); return date.getUTCFullYear()===y&&date.getUTCMonth()===m-1&&date.getUTCDate()===d; }
|
|
197
149
|
function inspect(text) {
|
|
198
|
-
const lines
|
|
199
|
-
const headings
|
|
200
|
-
if
|
|
201
|
-
if
|
|
202
|
-
const headingIndex
|
|
203
|
-
let
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const section =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
function nextVersion(ledger, date) {
|
|
223
|
-
if (!/^\d{8}$/.test(date) || !validDate(date)) fail(`invalid current release date: ${date}`);
|
|
224
|
-
const maximum = ledger.versions
|
|
225
|
-
.filter((item) => item.startsWith(`${date}-`))
|
|
226
|
-
.reduce((max, item) => Math.max(max, Number(item.slice(-3))), 0);
|
|
227
|
-
if (maximum >= 999) fail(`release sequence exhausted for ${date}`);
|
|
228
|
-
return `${date}-${String(maximum + 1).padStart(3, '0')}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const original = fs.readFileSync(agentsPath, 'utf8');
|
|
232
|
-
const ledger = inspect(original);
|
|
233
|
-
if (mode === 'next') {
|
|
234
|
-
process.stdout.write(`${nextVersion(ledger, value)}\n`);
|
|
235
|
-
} else if (mode === 'prepare') {
|
|
236
|
-
if (!versionPattern.test(value) || !validDate(value.slice(0, 8))) fail(`invalid release version: ${value}`);
|
|
237
|
-
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{2}:\d{2}$/.test(timestamp)) fail('invalid build time');
|
|
238
|
-
const expected = nextVersion(ledger, value.slice(0, 8));
|
|
239
|
-
if (value !== expected) fail(`release ledger changed: expected ${expected}, got ${value}`);
|
|
240
|
-
const row = `| ${value} | ${timestamp} |`;
|
|
241
|
-
let updated;
|
|
242
|
-
if (ledger.headingIndex === -1) {
|
|
243
|
-
const gap = original.length === 0 ? '' : original.endsWith('\n') ? '\n' : '\n\n';
|
|
244
|
-
updated = `${original}${gap}${heading}\n\n${header}\n${separator}\n${row}\n`;
|
|
245
|
-
} else {
|
|
246
|
-
const lines = [...ledger.lines];
|
|
247
|
-
let insertAt = ledger.endIndex;
|
|
248
|
-
while (insertAt > ledger.headingIndex + 1 && lines[insertAt - 1] === '') insertAt -= 1;
|
|
249
|
-
lines.splice(insertAt, 0, row);
|
|
250
|
-
updated = lines.join('\n');
|
|
251
|
-
}
|
|
252
|
-
fs.writeFileSync(outputPath, updated, { mode: fs.statSync(agentsPath).mode });
|
|
253
|
-
} else {
|
|
254
|
-
fail(`unknown release ledger mode: ${mode}`);
|
|
255
|
-
}
|
|
150
|
+
const lines=text.replace(/\r\n/g,'\n').split('\n');
|
|
151
|
+
const headings=lines.flatMap((line,index)=>line===heading?[index]:[]);
|
|
152
|
+
if(headings.length>1) fail('AGENTS.md contains multiple release record sections');
|
|
153
|
+
if(!headings.length) return {lines,headingIndex:-1,endIndex:lines.length,versions:[]};
|
|
154
|
+
const headingIndex=headings[0]; let endIndex=lines.length;
|
|
155
|
+
for(let index=headingIndex+1;index<lines.length;index+=1){if(/^## /.test(lines[index])){endIndex=index;break;}}
|
|
156
|
+
const section=lines.slice(headingIndex+1,endIndex); while(section[0]==='')section.shift(); while(section.at(-1)==='')section.pop();
|
|
157
|
+
if(section[0]!==header||section[1]!==separator) fail('AGENTS.md release record table is invalid');
|
|
158
|
+
const unique=new Set(),versions=[];
|
|
159
|
+
for(const row of section.slice(2)){const match=row.match(rowPattern);if(!match||!validDate(match[1].slice(0,8)))fail(`invalid AGENTS.md release record row: ${row}`);if(unique.has(match[1]))fail(`duplicate release version: ${match[1]}`);unique.add(match[1]);versions.push(match[1]);}
|
|
160
|
+
return {lines,headingIndex,endIndex,versions};
|
|
161
|
+
}
|
|
162
|
+
function nextVersion(ledger,date){if(!/^\d{8}$/.test(date)||!validDate(date))fail(`invalid current release date: ${date}`);const max=ledger.versions.filter(item=>item.startsWith(`${date}-`)).reduce((result,item)=>Math.max(result,Number(item.slice(-3))),0);if(max>=999)fail(`release sequence exhausted for ${date}`);return `${date}-${String(max+1).padStart(3,'0')}`;}
|
|
163
|
+
const original=fs.readFileSync(agentsPath,'utf8'); const ledger=inspect(original);
|
|
164
|
+
if(mode==='next') process.stdout.write(`${nextVersion(ledger,value)}\n`);
|
|
165
|
+
else if(mode==='prepare'){
|
|
166
|
+
if(!/^\d{8}-\d{3}$/.test(value)||!validDate(value.slice(0,8)))fail(`invalid release version: ${value}`);
|
|
167
|
+
if(!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{2}:\d{2}$/.test(timestamp))fail('invalid build time');
|
|
168
|
+
const expected=nextVersion(ledger,value.slice(0,8));if(value!==expected)fail(`release ledger changed: expected ${expected}, got ${value}`);
|
|
169
|
+
const row=`| ${value} | ${timestamp} |`;let updated;
|
|
170
|
+
if(ledger.headingIndex===-1){const gap=original.length===0?'':original.endsWith('\n')?'\n':'\n\n';updated=`${original}${gap}${heading}\n\n${header}\n${separator}\n${row}\n`;}
|
|
171
|
+
else{const lines=[...ledger.lines];let insertAt=ledger.endIndex;while(insertAt>ledger.headingIndex+1&&lines[insertAt-1]==='')insertAt-=1;lines.splice(insertAt,0,row);updated=lines.join('\n');}
|
|
172
|
+
fs.writeFileSync(outputPath,updated,{mode:fs.statSync(agentsPath).mode});
|
|
173
|
+
} else fail(`unknown release ledger mode: ${mode}`);
|
|
256
174
|
NODE
|
|
257
175
|
}
|
|
258
176
|
|
|
259
177
|
create_manifest() {
|
|
260
178
|
local version_root="$1" file relative checksum
|
|
261
|
-
(
|
|
262
|
-
cd "$version_root"
|
|
263
|
-
find . -type f ! -name manifest.sha256 -print | LC_ALL=C sort |
|
|
264
|
-
while IFS= read -r file; do
|
|
265
|
-
relative="${file#./}"
|
|
266
|
-
checksum="$(checksum_value "$file")"
|
|
267
|
-
printf '%s %s\n' "$checksum" "$relative"
|
|
268
|
-
done > manifest.sha256
|
|
269
|
-
)
|
|
179
|
+
(cd "$version_root"; find . -type f ! -name manifest.sha256 -print | LC_ALL=C sort | while IFS= read -r file; do relative="${file#./}"; checksum="$(checksum_value "$file")"; printf '%s %s\n' "$checksum" "$relative"; done > manifest.sha256)
|
|
270
180
|
}
|
|
271
181
|
|
|
272
182
|
verify_manifest() {
|
|
273
|
-
local
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
printf '%s\n' "$relative" >> "$declared_paths"
|
|
286
|
-
[[ -f "$relative" && ! -L "$relative" ]] || exit 12
|
|
287
|
-
actual="$(checksum_value "$relative")"
|
|
288
|
-
[[ "$actual" == "$checksum" ]] || exit 13
|
|
289
|
-
done < manifest.sha256
|
|
290
|
-
LC_ALL=C sort -o "$declared_paths" "$declared_paths"
|
|
291
|
-
[[ "$(uniq -d "$declared_paths" | wc -l | tr -d ' ')" == 0 ]] || exit 14
|
|
292
|
-
cmp -s "$actual_paths" "$declared_paths"
|
|
293
|
-
); then
|
|
294
|
-
result=0
|
|
295
|
-
else
|
|
296
|
-
result=$?
|
|
297
|
-
fi
|
|
298
|
-
rm -f -- "$actual_paths" "$declared_paths"
|
|
299
|
-
[[ "$result" -eq 0 ]] || fail "release manifest validation failed"
|
|
183
|
+
local root="$1" line checksum relative actual
|
|
184
|
+
[[ -s "$root/manifest.sha256" ]] || fail "missing or empty manifest.sha256"
|
|
185
|
+
while IFS= read -r line; do
|
|
186
|
+
checksum="${line%% *}"; relative="${line#* }"
|
|
187
|
+
[[ "$checksum" =~ ^[0-9a-f]{64}$ && "$relative" != "$line" && "$relative" != /* && "$relative" != ../* && "$relative" != *'/../'* ]] || fail "invalid manifest entry"
|
|
188
|
+
[[ -f "$root/$relative" && ! -L "$root/$relative" ]] || fail "manifest path is missing: $relative"
|
|
189
|
+
actual="$(checksum_value "$root/$relative")"; [[ "$actual" == "$checksum" ]] || fail "checksum mismatch: $relative"
|
|
190
|
+
done < "$root/manifest.sha256"
|
|
191
|
+
local actual_paths declared_paths
|
|
192
|
+
actual_paths="$(cd "$root"; find . -type f ! -name manifest.sha256 -print | sed 's|^./||' | LC_ALL=C sort)"
|
|
193
|
+
declared_paths="$(sed -n 's/^[0-9a-f]\{64\} //p' "$root/manifest.sha256" | LC_ALL=C sort)"
|
|
194
|
+
[[ "$actual_paths" == "$declared_paths" ]] || fail "manifest coverage is incomplete"
|
|
300
195
|
}
|
|
301
196
|
|
|
302
197
|
reject_version_secrets() {
|
|
303
198
|
local root="$1" file base
|
|
304
|
-
|
|
305
|
-
while IFS= read -r file; do
|
|
306
|
-
base="${file##*/}"
|
|
307
|
-
case "$base" in
|
|
308
|
-
.env|.env.*|*.pem|*.key|id_rsa|id_ed25519|credentials|credentials.*)
|
|
309
|
-
fail "secret file must not enter the version directory: ${file#"$root"/}" ;;
|
|
310
|
-
esac
|
|
311
|
-
done < <(find "$root" -type f -print)
|
|
199
|
+
! find "$root" -type l -print -quit | grep -q . || fail "symbolic links must not be packaged"
|
|
200
|
+
while IFS= read -r file; do base="${file##*/}"; case "$base" in .env|.env.*|*.pem|*.key|id_rsa|id_ed25519|credentials|credentials.*) fail "secret file must not enter version directory: ${file#"$root"/}" ;; esac; done < <(find "$root" -type f -print)
|
|
312
201
|
}
|
|
313
202
|
|
|
314
203
|
validate_dockerignore() {
|
|
315
204
|
local file="$1" pattern line
|
|
316
205
|
[[ -f "$file" ]] || fail "required Docker ignore file is missing: $file"
|
|
317
|
-
for pattern in '.env' '.env.*' '*.pem' '*.key' id_rsa id_ed25519 credentials 'credentials.*'
|
|
318
|
-
|
|
319
|
-
grep -Fxq "$pattern" "$file" || fail "Docker ignore file is missing required pattern: $pattern"
|
|
320
|
-
done
|
|
321
|
-
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
322
|
-
[[ "$line" != !* || "$line" == '!.env.example' ]] || fail "Docker ignore file contains forbidden reverse include: $line"
|
|
323
|
-
done < "$file"
|
|
206
|
+
for pattern in '.env' '.env.*' '*.pem' '*.key' id_rsa id_ed25519 credentials 'credentials.*' 'deploy/' 'logs/' '.git/' 'node_modules/' 'venv/' '.pnpm-store/' '.deploy-build.lock/' '.deploy-build.*'; do grep -Fxq "$pattern" "$file" || fail "Docker ignore file is missing required pattern: $pattern"; done
|
|
207
|
+
while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" != !* || "$line" == '!.env.example' ]] || fail "Docker ignore file contains forbidden reverse include: $line"; done < "$file"
|
|
324
208
|
}
|
|
325
209
|
|
|
326
210
|
validate_local_package() {
|
|
327
|
-
local output_root="$1" version="$2" version_root="$1/$2"
|
|
211
|
+
local output_root="$1" version="$2" version_root="$1/$2" compose="$1/$2/docker/docker-compose.yml"
|
|
328
212
|
[[ -x "$output_root/deploy.sh" ]] || fail "generated deploy.sh is missing or not executable"
|
|
329
213
|
cmp -s "$SCRIPT_PATH" "$output_root/deploy.sh" || fail "generated deploy.sh differs from its source"
|
|
330
214
|
cmp -s "$SCRIPT_DIR/.env" "$output_root/.env" || fail "generated root .env differs from project .env"
|
|
331
215
|
[[ "$(cat "$output_root/release.env")" == "RELEASE_VERSION=$version" ]] || fail "invalid release.env"
|
|
332
|
-
[[ -f "$version_root/frontend-image.tar" && -f "$version_root/backend-image.tar" ]] || fail "dual image
|
|
333
|
-
[[ -
|
|
334
|
-
[[ -
|
|
335
|
-
|
|
336
|
-
grep -Fq 'frontend-blue:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing frontend-blue"
|
|
337
|
-
grep -Fq 'backend-blue:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing backend-blue"
|
|
338
|
-
grep -Fq 'frontend-green:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing frontend-green"
|
|
339
|
-
grep -Fq 'backend-green:' "$version_root/docker/docker-compose.blue-green.yml" || fail "missing backend-green"
|
|
340
|
-
grep -Fq '__PROJECT_HTTP_PORT__' "$version_root/nginx/site.conf" || fail "missing project port placeholder"
|
|
216
|
+
[[ -f "$version_root/frontend-image.tar" && -f "$version_root/backend-image.tar" && -f "$compose" ]] || fail "dual image package is incomplete"
|
|
217
|
+
[[ "$(grep -Ec '^ (frontend|backend):$' "$compose")" == 2 ]] || fail "Compose must define frontend and backend only"
|
|
218
|
+
[[ "$(grep -Fc '../../logs:/app/logs' "$compose")" == 2 ]] || fail "both services must mount root logs"
|
|
219
|
+
! grep -Eqi 'blue|green|nginx-upstream|PROJECT_HTTP_PORT' "$compose" || fail "blue-green or host Nginx configuration is forbidden"
|
|
341
220
|
[[ ! -e "$version_root/database" ]] || fail "release package must not contain database migration files"
|
|
342
|
-
|
|
343
|
-
reject_version_secrets "$version_root"
|
|
344
|
-
verify_manifest "$version_root"
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
acquire_build_lock() {
|
|
348
|
-
BUILD_LOCK="$SCRIPT_DIR/.deploy-build.lock"
|
|
349
|
-
mkdir "$BUILD_LOCK" 2>/dev/null || fail "another build is active or stale lock exists: $BUILD_LOCK"
|
|
221
|
+
reject_version_secrets "$version_root"; verify_manifest "$version_root"
|
|
350
222
|
}
|
|
351
223
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
224
|
+
acquire_build_lock() { BUILD_LOCK="$SCRIPT_DIR/.deploy-build.lock"; mkdir "$BUILD_LOCK" 2>/dev/null || fail "another build is active or stale lock exists: $BUILD_LOCK"; }
|
|
225
|
+
release_build_lock() { [[ -n "${BUILD_LOCK:-}" && -d "$BUILD_LOCK" ]] && rmdir "$BUILD_LOCK"; }
|
|
355
226
|
|
|
356
227
|
build_release() {
|
|
357
|
-
local project_root="$SCRIPT_DIR" agents_file="$SCRIPT_DIR/AGENTS.md"
|
|
358
|
-
local
|
|
359
|
-
|
|
360
|
-
local staging_parent output_root version_root agents_prepared old_deploy
|
|
361
|
-
|
|
362
|
-
acquire_build_lock
|
|
363
|
-
staging_parent=
|
|
364
|
-
agents_prepared=
|
|
365
|
-
old_deploy=
|
|
228
|
+
local project_root="$SCRIPT_DIR" agents_file="$SCRIPT_DIR/AGENTS.md" today version raw_time build_time frontend_image backend_image
|
|
229
|
+
local frontend_dockerfile backend_dockerfile dockerignore_file deploy_config_dir compose_source staging_parent output_root version_root agents_prepared old_deploy
|
|
230
|
+
acquire_build_lock; staging_parent=; agents_prepared=; old_deploy=
|
|
366
231
|
trap 'rm -rf -- "${staging_parent:-}" "${old_deploy:-}"; rm -f -- "${agents_prepared:-}"; release_build_lock' EXIT
|
|
367
232
|
[[ -r "$agents_file" && -w "$agents_file" ]] || fail "project AGENTS.md must be readable and writable"
|
|
368
|
-
load_dotenv "$project_root/.env"
|
|
369
|
-
validate_configuration
|
|
370
|
-
|
|
233
|
+
load_dotenv "$project_root/.env"; validate_configuration
|
|
371
234
|
frontend_dockerfile="$(resolve_project_path "$project_root" "${FRONTEND_DOCKERFILE:-src/frontend/Dockerfile}")"
|
|
372
235
|
backend_dockerfile="$(resolve_project_path "$project_root" "${BACKEND_DOCKERFILE:-src/backend/Dockerfile}")"
|
|
373
236
|
dockerignore_file="$(resolve_project_path "$project_root" "${DOCKERIGNORE_FILE:-.dockerignore}")"
|
|
374
237
|
deploy_config_dir="$(resolve_project_path "$project_root" "${DEPLOY_CONFIG_DIR:-scripts/deployment}")"
|
|
375
|
-
compose_source="$(resolve_project_path "$project_root" "${COMPOSE_SOURCE:-${deploy_config_dir#"$project_root"/}/docker-compose.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
for command in node pnpm docker tar install find sort cmp awk sed grep uniq; do require_command "$command"; done
|
|
238
|
+
compose_source="$(resolve_project_path "$project_root" "${COMPOSE_SOURCE:-${deploy_config_dir#"$project_root"/}/docker-compose.yml}")"
|
|
239
|
+
for command in node pnpm docker install find sort cmp awk sed grep; do require_command "$command"; done
|
|
379
240
|
select_checksum_tool
|
|
380
|
-
[[ -f "$frontend_dockerfile" && -f "$backend_dockerfile" ]] || fail "
|
|
381
|
-
[[ -f "$compose_source" && -f "$nginx_source" ]] || fail "deployment Compose and Nginx sources are required"
|
|
241
|
+
[[ -f "$frontend_dockerfile" && -f "$backend_dockerfile" && -f "$compose_source" ]] || fail "Dockerfiles and deployment Compose are required"
|
|
382
242
|
validate_dockerignore "$dockerignore_file"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
version="$(release_ledger next "$agents_file" "$today")"
|
|
386
|
-
assert_version "$version"
|
|
387
|
-
frontend_image="${PROJECT_NAME}_frontend:$version"
|
|
388
|
-
backend_image="${PROJECT_NAME}_backend:$version"
|
|
389
|
-
|
|
243
|
+
today="$(date +%Y%m%d)"; version="$(release_ledger next "$agents_file" "$today")"; assert_version "$version"
|
|
244
|
+
frontend_image="${PROJECT_NAME}_frontend:$version"; backend_image="${PROJECT_NAME}_backend:$version"
|
|
390
245
|
echo "release gate: pnpm test"; pnpm test
|
|
391
246
|
echo "release gate: pnpm lint"; pnpm lint
|
|
392
247
|
echo "release gate: pnpm typecheck"; pnpm typecheck
|
|
393
248
|
echo "release gate: pnpm build"; RELEASE_VERSION="$version" pnpm build
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
docker build --build-arg "FRONTEND_BASE_IMAGE=$FRONTEND_BASE_IMAGE" --build-arg "RELEASE_VERSION=$version" \
|
|
397
|
-
--tag "$frontend_image" --file "$frontend_dockerfile" "$project_root"
|
|
398
|
-
echo "release gate: backend docker build"
|
|
399
|
-
docker build --build-arg "BACKEND_BASE_IMAGE=$BACKEND_BASE_IMAGE" --build-arg "RELEASE_VERSION=$version" \
|
|
400
|
-
--build-arg "BACKEND_PORT=$BACKEND_PORT" --tag "$backend_image" --file "$backend_dockerfile" "$project_root"
|
|
249
|
+
docker build --build-arg "FRONTEND_BASE_IMAGE=$FRONTEND_BASE_IMAGE" --build-arg "RELEASE_VERSION=$version" --tag "$frontend_image" --file "$frontend_dockerfile" "$project_root"
|
|
250
|
+
docker build --build-arg "BACKEND_BASE_IMAGE=$BACKEND_BASE_IMAGE" --build-arg "RELEASE_VERSION=$version" --build-arg "BACKEND_PORT=$BACKEND_PORT" --tag "$backend_image" --file "$backend_dockerfile" "$project_root"
|
|
401
251
|
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$frontend_image")" == "$version" ]] || fail "frontend image version label mismatch"
|
|
402
252
|
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$backend_image")" == "$version" ]] || fail "backend image version label mismatch"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
output_root="$staging_parent/deploy"
|
|
406
|
-
version_root="$output_root/$version"
|
|
407
|
-
agents_prepared="$project_root/.AGENTS.md.release.$$.tmp"
|
|
408
|
-
mkdir -p "$version_root/docker" "$version_root/nginx"
|
|
409
|
-
install -m 0755 "$SCRIPT_PATH" "$output_root/deploy.sh"
|
|
410
|
-
install -m 0600 "$project_root/.env" "$output_root/.env"
|
|
253
|
+
staging_parent="$(mktemp -d "$project_root/.deploy-build.XXXXXX")"; output_root="$staging_parent/deploy"; version_root="$output_root/$version"; agents_prepared="$project_root/.AGENTS.md.release.$$.tmp"
|
|
254
|
+
mkdir -p "$version_root/docker"; install -m 0755 "$SCRIPT_PATH" "$output_root/deploy.sh"; install -m 0600 "$project_root/.env" "$output_root/.env"
|
|
411
255
|
printf 'RELEASE_VERSION=%s\n' "$version" > "$output_root/release.env"
|
|
412
|
-
docker save -o "$version_root/frontend-image.tar" "$frontend_image"
|
|
413
|
-
docker
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
cmp -s "$compose_source" "$version_root/docker/docker-compose.blue-green.yml" || fail "generated Compose is not deterministic"
|
|
417
|
-
cmp -s "$nginx_source" "$version_root/nginx/site.conf" || fail "generated Nginx config is not deterministic"
|
|
418
|
-
create_manifest "$version_root"
|
|
419
|
-
validate_local_package "$output_root" "$version"
|
|
420
|
-
|
|
421
|
-
raw_time="$(date '+%Y-%m-%d %H:%M:%S %z')"
|
|
422
|
-
build_time="${raw_time%?????}${raw_time: -5:3}:${raw_time: -2}"
|
|
256
|
+
docker save -o "$version_root/frontend-image.tar" "$frontend_image"; docker save -o "$version_root/backend-image.tar" "$backend_image"
|
|
257
|
+
install -m 0644 "$compose_source" "$version_root/docker/docker-compose.yml"; cmp -s "$compose_source" "$version_root/docker/docker-compose.yml" || fail "generated Compose is not deterministic"
|
|
258
|
+
create_manifest "$version_root"; validate_local_package "$output_root" "$version"
|
|
259
|
+
raw_time="$(date '+%Y-%m-%d %H:%M:%S %z')"; build_time="${raw_time%?????}${raw_time: -5:3}:${raw_time: -2}"
|
|
423
260
|
release_ledger prepare "$agents_file" "$version" "$build_time" "$agents_prepared"
|
|
424
|
-
|
|
425
|
-
if [[ -
|
|
426
|
-
|
|
427
|
-
[[ ! -e "$old_deploy" ]] || fail "temporary deploy replacement path already exists"
|
|
428
|
-
mv "$project_root/deploy" "$old_deploy"
|
|
429
|
-
fi
|
|
430
|
-
if ! mv "$output_root" "$project_root/deploy"; then
|
|
431
|
-
[[ -n "$old_deploy" ]] && mv "$old_deploy" "$project_root/deploy"
|
|
432
|
-
fail "failed to publish deploy directory"
|
|
433
|
-
fi
|
|
434
|
-
if ! mv "$agents_prepared" "$agents_file"; then
|
|
435
|
-
rm -rf -- "$project_root/deploy"
|
|
436
|
-
[[ -n "$old_deploy" ]] && mv "$old_deploy" "$project_root/deploy"
|
|
437
|
-
fail "failed to write AGENTS.md release record"
|
|
438
|
-
fi
|
|
261
|
+
if [[ -e "$project_root/deploy" ]]; then old_deploy="$project_root/.deploy.previous.$$"; [[ ! -e "$old_deploy" ]] || fail "temporary deploy replacement path already exists"; mv "$project_root/deploy" "$old_deploy"; fi
|
|
262
|
+
if ! mv "$output_root" "$project_root/deploy"; then [[ -n "$old_deploy" ]] && mv "$old_deploy" "$project_root/deploy"; fail "failed to publish deploy directory"; fi
|
|
263
|
+
if ! mv "$agents_prepared" "$agents_file"; then rm -rf -- "$project_root/deploy"; [[ -n "$old_deploy" ]] && mv "$old_deploy" "$project_root/deploy"; fail "failed to write AGENTS.md release record"; fi
|
|
439
264
|
[[ -n "$old_deploy" ]] && rm -rf -- "$old_deploy" && old_deploy=
|
|
440
|
-
rm -rf -- "$staging_parent"; staging_parent
|
|
441
|
-
release_build_lock; BUILD_LOCK=
|
|
442
|
-
trap - EXIT
|
|
265
|
+
rm -rf -- "$staging_parent"; staging_parent=; release_build_lock; BUILD_LOCK=; trap - EXIT
|
|
443
266
|
echo "release package created: deploy/$version"
|
|
444
|
-
return 0
|
|
445
267
|
}
|
|
446
268
|
|
|
447
269
|
read_release_pointer() {
|
|
448
270
|
local file="$DEPLOY_ROOT/release.env" line
|
|
449
271
|
[[ -f "$file" && "$(wc -l < "$file" | tr -d ' ')" == 1 ]] || fail "release.env must contain exactly one line"
|
|
450
|
-
line="$(cat "$file")"
|
|
451
|
-
|
|
452
|
-
RELEASE_VERSION="${BASH_REMATCH[1]}"
|
|
453
|
-
assert_version "$RELEASE_VERSION"
|
|
272
|
+
line="$(cat "$file")"; [[ "$line" =~ ^RELEASE_VERSION=([0-9]{8}-[0-9]{3})$ ]] || fail "invalid release.env"
|
|
273
|
+
RELEASE_VERSION="${BASH_REMATCH[1]}"; assert_version "$RELEASE_VERSION"
|
|
454
274
|
}
|
|
455
275
|
|
|
456
276
|
initialize_server_context() {
|
|
457
|
-
DEPLOY_ROOT="$SCRIPT_DIR"
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
STATE_DIR="$DEPLOY_ROOT/state"
|
|
461
|
-
ACTIVE_STATE="$STATE_DIR/active.env"
|
|
462
|
-
PREVIOUS_STATE="$STATE_DIR/previous.env"
|
|
463
|
-
NGINX_DIR="$DEPLOY_ROOT/nginx"
|
|
464
|
-
NGINX_SITE_FILE="$NGINX_DIR/site.conf"
|
|
465
|
-
UPSTREAM_FILE="$NGINX_DIR/active-upstreams.conf"
|
|
466
|
-
FRONTEND_UPSTREAM_NAME="${PROJECT_NAME}_frontend_active"
|
|
467
|
-
BACKEND_UPSTREAM_NAME="${PROJECT_NAME}_backend_active"
|
|
468
|
-
FRONTEND_IMAGE_REPOSITORY="${PROJECT_NAME}_frontend"
|
|
469
|
-
BACKEND_IMAGE_REPOSITORY="${PROJECT_NAME}_backend"
|
|
470
|
-
for command in docker curl nginx tar find sort cmp awk sed grep uniq flock; do require_command "$command"; done
|
|
471
|
-
select_checksum_tool
|
|
277
|
+
DEPLOY_ROOT="$SCRIPT_DIR"; load_dotenv "$DEPLOY_ROOT/.env"; validate_configuration; read_release_pointer
|
|
278
|
+
for command in docker curl tar find sort awk sed grep; do require_command "$command"; done
|
|
279
|
+
select_checksum_tool; mkdir -p "$DEPLOY_ROOT/logs"
|
|
472
280
|
}
|
|
473
281
|
|
|
474
|
-
|
|
475
|
-
local
|
|
476
|
-
[[ -
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"; count=$((count + 1))
|
|
480
|
-
if [[ "$key" == ACTIVE_COLOR ]]; then [[ -z "$color" ]] || fail "duplicate state color"; color="$value"; else [[ -z "$version" ]] || fail "duplicate state version"; version="$value"; fi
|
|
481
|
-
done < "$file"
|
|
482
|
-
[[ "$count" -eq 2 && ( "$color" == blue || "$color" == green ) ]] || fail "incomplete state file: $file"
|
|
483
|
-
assert_version "$version"
|
|
484
|
-
STATE_COLOR="$color"; STATE_VERSION="$version"
|
|
282
|
+
record_operation() {
|
|
283
|
+
local result="$1" timestamp
|
|
284
|
+
[[ -n "$DEPLOY_ROOT" && -d "$DEPLOY_ROOT/logs" && -n "$OPERATION" ]] || return 0
|
|
285
|
+
timestamp="$(date '+%Y-%m-%d %H:%M:%S %z')"
|
|
286
|
+
printf '%s version=%s operation=%s result=%s\n' "$timestamp" "${RELEASE_VERSION:-unknown}" "$OPERATION" "$result" >> "$DEPLOY_ROOT/logs/deploy.log"
|
|
485
287
|
}
|
|
486
288
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
289
|
+
validate_uploaded_package() {
|
|
290
|
+
local root="$DEPLOY_ROOT/$RELEASE_VERSION"
|
|
291
|
+
[[ -f "$root/frontend-image.tar" && -f "$root/backend-image.tar" && -f "$root/docker/docker-compose.yml" ]] || fail "uploaded dual image package is incomplete"
|
|
292
|
+
[[ ! -e "$root/database" ]] || fail "release package must not contain database migration files"
|
|
293
|
+
reject_version_secrets "$root"; verify_manifest "$root"
|
|
294
|
+
tar -tf "$root/frontend-image.tar" >/dev/null || fail "invalid frontend image archive"
|
|
295
|
+
tar -tf "$root/backend-image.tar" >/dev/null || fail "invalid backend image archive"
|
|
490
296
|
}
|
|
491
297
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
298
|
+
archive_has_tag() {
|
|
299
|
+
local archive="$1" tag="$2" manifest compact count
|
|
300
|
+
manifest="$(tar -xOf "$archive" manifest.json 2>/dev/null)" || fail "image archive is missing manifest.json: $archive"
|
|
301
|
+
compact="$(tr -d '[:space:]' <<< "$manifest")"; count="$(grep -o '"RepoTags"' <<< "$compact" | wc -l | tr -d ' ')"
|
|
302
|
+
[[ "$count" == 1 && "$compact" == *"\"RepoTags\":[\"$tag\"]"* ]] || fail "image archive must contain exactly one expected tag: $tag"
|
|
495
303
|
}
|
|
496
304
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
305
|
+
image_names() { FRONTEND_IMAGE="${PROJECT_NAME}_frontend:$RELEASE_VERSION"; BACKEND_IMAGE="${PROJECT_NAME}_backend:$RELEASE_VERSION"; export FRONTEND_IMAGE BACKEND_IMAGE RELEASE_VERSION; }
|
|
306
|
+
|
|
307
|
+
load_frontend_image() {
|
|
308
|
+
local archive="$DEPLOY_ROOT/$RELEASE_VERSION/frontend-image.tar"
|
|
309
|
+
archive_has_tag "$archive" "$FRONTEND_IMAGE"; docker load -i "$archive" >/dev/null
|
|
310
|
+
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$FRONTEND_IMAGE")" == "$RELEASE_VERSION" ]] || fail "frontend loaded image version mismatch"
|
|
500
311
|
}
|
|
501
312
|
|
|
502
|
-
|
|
503
|
-
local
|
|
504
|
-
|
|
505
|
-
|
|
313
|
+
load_backend_image() {
|
|
314
|
+
local archive="$DEPLOY_ROOT/$RELEASE_VERSION/backend-image.tar"
|
|
315
|
+
archive_has_tag "$archive" "$BACKEND_IMAGE"; docker load -i "$archive" >/dev/null
|
|
316
|
+
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$BACKEND_IMAGE")" == "$RELEASE_VERSION" ]] || fail "backend loaded image version mismatch"
|
|
506
317
|
}
|
|
507
318
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
assert_version "$version"
|
|
511
|
-
FRONTEND_IMAGE="${FRONTEND_IMAGE_REPOSITORY}:$version"
|
|
512
|
-
BACKEND_IMAGE="${BACKEND_IMAGE_REPOSITORY}:$version"
|
|
319
|
+
compose() {
|
|
320
|
+
FRONTEND_IMAGE="$FRONTEND_IMAGE" BACKEND_IMAGE="$BACKEND_IMAGE" RELEASE_VERSION="$RELEASE_VERSION" docker compose --env-file "$DEPLOY_ROOT/.env" --project-name "$PROJECT_NAME" -f "$DEPLOY_ROOT/$RELEASE_VERSION/docker/docker-compose.yml" "$@"
|
|
513
321
|
}
|
|
514
322
|
|
|
515
|
-
|
|
516
|
-
local
|
|
517
|
-
[[
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
tar -tf "$package_root/frontend-image.tar" >/dev/null || fail "invalid frontend image archive"
|
|
523
|
-
tar -tf "$package_root/backend-image.tar" >/dev/null || fail "invalid backend image archive"
|
|
323
|
+
verify_service() {
|
|
324
|
+
local service="$1" port health version_url actual
|
|
325
|
+
if [[ "$service" == frontend ]]; then port="$FRONTEND_PORT"; else port="$BACKEND_PORT"; fi
|
|
326
|
+
health="$(endpoint_url "$DOCKER_BIND_IP" "$port" /health)"; version_url="$(endpoint_url "$DOCKER_BIND_IP" "$port" /version)"
|
|
327
|
+
curl --fail --silent --show-error --retry 12 --retry-delay 2 --retry-connrefused "$health" >/dev/null
|
|
328
|
+
actual="$(curl --fail --silent --show-error "$version_url")"
|
|
329
|
+
[[ "$actual" == "$RELEASE_VERSION" ]] || fail "$service version mismatch: expected $RELEASE_VERSION, got $actual"
|
|
524
330
|
}
|
|
525
331
|
|
|
526
|
-
|
|
527
|
-
local
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
local version="$1" package_root="$DEPLOY_ROOT/$1"
|
|
537
|
-
image_names_for "$version"
|
|
538
|
-
archive_has_tag "$package_root/frontend-image.tar" "$FRONTEND_IMAGE"
|
|
539
|
-
archive_has_tag "$package_root/backend-image.tar" "$BACKEND_IMAGE"
|
|
540
|
-
docker load -i "$package_root/frontend-image.tar" >/dev/null
|
|
541
|
-
docker load -i "$package_root/backend-image.tar" >/dev/null
|
|
542
|
-
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$FRONTEND_IMAGE")" == "$version" ]] || fail "frontend loaded image version mismatch"
|
|
543
|
-
[[ "$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$BACKEND_IMAGE")" == "$version" ]] || fail "backend loaded image version mismatch"
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
compose_for() {
|
|
547
|
-
local version="$1"; shift
|
|
548
|
-
local compose_file
|
|
549
|
-
image_names_for "$version"
|
|
550
|
-
compose_file="$(compose_file_for "$version")"
|
|
551
|
-
[[ -f "$compose_file" ]] || fail "compose file is missing for version $version"
|
|
552
|
-
FRONTEND_IMAGE="$FRONTEND_IMAGE" BACKEND_IMAGE="$BACKEND_IMAGE" \
|
|
553
|
-
docker compose --env-file "$DEPLOY_ROOT/.env" --project-name "$PROJECT_NAME" -f "$compose_file" "$@"
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
up_pair() {
|
|
557
|
-
local color="$1" version="$2"
|
|
558
|
-
compose_for "$version" up -d --force-recreate "frontend-$color" "backend-$color"
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
verify_pair() {
|
|
562
|
-
local color="$1" version="$2" frontend_health frontend_version backend_health backend_version frontend_actual backend_actual
|
|
563
|
-
if [[ "$color" == blue ]]; then
|
|
564
|
-
frontend_health="$(endpoint_url "$DOCKER_BIND_IP" "$FRONTEND_BLUE_PORT" /health)"; frontend_version="$(endpoint_url "$DOCKER_BIND_IP" "$FRONTEND_BLUE_PORT" /version)"
|
|
565
|
-
backend_health="$(endpoint_url "$DOCKER_BIND_IP" "$BACKEND_BLUE_PORT" /health)"; backend_version="$(endpoint_url "$DOCKER_BIND_IP" "$BACKEND_BLUE_PORT" /version)"
|
|
566
|
-
else
|
|
567
|
-
frontend_health="$(endpoint_url "$DOCKER_BIND_IP" "$FRONTEND_GREEN_PORT" /health)"; frontend_version="$(endpoint_url "$DOCKER_BIND_IP" "$FRONTEND_GREEN_PORT" /version)"
|
|
568
|
-
backend_health="$(endpoint_url "$DOCKER_BIND_IP" "$BACKEND_GREEN_PORT" /health)"; backend_version="$(endpoint_url "$DOCKER_BIND_IP" "$BACKEND_GREEN_PORT" /version)"
|
|
569
|
-
fi
|
|
570
|
-
curl --fail --silent --show-error --retry 12 --retry-delay 2 --retry-connrefused "$frontend_health" >/dev/null
|
|
571
|
-
curl --fail --silent --show-error --retry 12 --retry-delay 2 --retry-connrefused "$backend_health" >/dev/null
|
|
572
|
-
frontend_actual="$(curl --fail --silent --show-error "$frontend_version")"
|
|
573
|
-
backend_actual="$(curl --fail --silent --show-error "$backend_version")"
|
|
574
|
-
[[ "$frontend_actual" == "$version" ]] || fail "frontend version mismatch: expected $version, got $frontend_actual"
|
|
575
|
-
[[ "$backend_actual" == "$version" ]] || fail "backend version mismatch: expected $version, got $backend_actual"
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
render_site_candidate() {
|
|
579
|
-
local source="$1" output="$2" escaped_root
|
|
580
|
-
for placeholder in __PROJECT_NAME__ __PROJECT_HTTP_PORT__ __DEPLOY_ROOT__ __FRONTEND_UPSTREAM_NAME__ __BACKEND_UPSTREAM_NAME__; do
|
|
581
|
-
grep -Fq "$placeholder" "$source" || fail "Nginx template is missing $placeholder"
|
|
582
|
-
done
|
|
583
|
-
escaped_root="${DEPLOY_ROOT//\\/\\\\}"; escaped_root="${escaped_root//&/\\&}"; escaped_root="${escaped_root//|/\\|}"
|
|
584
|
-
sed -e "s|__PROJECT_NAME__|$PROJECT_NAME|g" -e "s|__PROJECT_HTTP_PORT__|$PROJECT_HTTP_PORT|g" \
|
|
585
|
-
-e "s|__DEPLOY_ROOT__|$escaped_root|g" -e "s|__FRONTEND_UPSTREAM_NAME__|$FRONTEND_UPSTREAM_NAME|g" \
|
|
586
|
-
-e "s|__BACKEND_UPSTREAM_NAME__|$BACKEND_UPSTREAM_NAME|g" "$source" > "$output"
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
write_upstream_candidate() {
|
|
590
|
-
local color="$1" output="$2" frontend_port backend_port
|
|
591
|
-
if [[ "$color" == blue ]]; then frontend_port="$FRONTEND_BLUE_PORT"; backend_port="$BACKEND_BLUE_PORT"; else frontend_port="$FRONTEND_GREEN_PORT"; backend_port="$BACKEND_GREEN_PORT"; fi
|
|
592
|
-
printf 'upstream %s {\n server %s:%s;\n keepalive 32;\n}\n\nupstream %s {\n server %s:%s;\n keepalive 32;\n}\n' \
|
|
593
|
-
"$FRONTEND_UPSTREAM_NAME" "$DOCKER_BIND_IP" "$frontend_port" \
|
|
594
|
-
"$BACKEND_UPSTREAM_NAME" "$DOCKER_BIND_IP" "$backend_port" > "$output"
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
restore_file() {
|
|
598
|
-
local backup="$1" target="$2"
|
|
599
|
-
if [[ -f "$backup" ]]; then cp -p "$backup" "$target"; else rm -f -- "$target"; fi
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
commit_traffic_and_state() {
|
|
603
|
-
local site_candidate="$1" upstream_candidate="$2" active_candidate="$3" previous_candidate="${4:-}" expected_version="$5"
|
|
604
|
-
local transaction public_health public_version_url public_version rollback_failed=0
|
|
605
|
-
mkdir -p "$NGINX_DIR" "$STATE_DIR"
|
|
606
|
-
transaction="$(mktemp -d "$DEPLOY_ROOT/.nginx-switch.XXXXXX")"
|
|
607
|
-
[[ -f "$NGINX_SITE_FILE" ]] && cp -p "$NGINX_SITE_FILE" "$transaction/site.old"
|
|
608
|
-
[[ -f "$UPSTREAM_FILE" ]] && cp -p "$UPSTREAM_FILE" "$transaction/upstreams.old"
|
|
609
|
-
[[ -f "$ACTIVE_STATE" ]] && cp -p "$ACTIVE_STATE" "$transaction/active.old"
|
|
610
|
-
[[ -f "$PREVIOUS_STATE" ]] && cp -p "$PREVIOUS_STATE" "$transaction/previous.old"
|
|
611
|
-
|
|
612
|
-
exec 9>/tmp/wdyy-nginx-deploy.lock
|
|
613
|
-
flock -x 9
|
|
614
|
-
install -m 0644 "$site_candidate" "$NGINX_SITE_FILE"
|
|
615
|
-
install -m 0644 "$upstream_candidate" "$UPSTREAM_FILE"
|
|
616
|
-
if ! nginx -t; then
|
|
617
|
-
restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
|
|
618
|
-
restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
|
|
619
|
-
rm -rf -- "$transaction"
|
|
620
|
-
fail "Nginx candidate validation failed; project configuration restored"
|
|
621
|
-
fi
|
|
622
|
-
if ! nginx -s reload; then
|
|
623
|
-
restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
|
|
624
|
-
restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
|
|
625
|
-
nginx -t && nginx -s reload || rollback_failed=1
|
|
626
|
-
rm -rf -- "$transaction"
|
|
627
|
-
[[ "$rollback_failed" -eq 0 ]] || fail "Nginx reload failed and old project configuration could not be restored"
|
|
628
|
-
fail "Nginx reload failed; old project configuration restored"
|
|
629
|
-
fi
|
|
332
|
+
run_or_replace() {
|
|
333
|
+
local target="$1"
|
|
334
|
+
validate_uploaded_package; image_names
|
|
335
|
+
case "$target" in
|
|
336
|
+
frontend) load_frontend_image; compose up -d --force-recreate --no-deps frontend; verify_service frontend ;;
|
|
337
|
+
backend) load_backend_image; compose up -d --force-recreate --no-deps backend; verify_service backend ;;
|
|
338
|
+
both) load_frontend_image; load_backend_image; compose up -d --force-recreate frontend backend; verify_service frontend; verify_service backend ;;
|
|
339
|
+
*) fail "invalid deployment target: $target" ;;
|
|
340
|
+
esac
|
|
341
|
+
}
|
|
630
342
|
|
|
631
|
-
|
|
632
|
-
public_version_url="$(endpoint_url "$DOCKER_BIND_IP" "$PROJECT_HTTP_PORT" /api/version)"
|
|
633
|
-
if ! curl --fail --silent --show-error --retry 6 --retry-delay 1 --retry-connrefused "$public_health" >/dev/null ||
|
|
634
|
-
! public_version="$(curl --fail --silent --show-error "$public_version_url")" ||
|
|
635
|
-
[[ "$public_version" != "$expected_version" ]]; then
|
|
636
|
-
restore_file "$transaction/site.old" "$NGINX_SITE_FILE"
|
|
637
|
-
restore_file "$transaction/upstreams.old" "$UPSTREAM_FILE"
|
|
638
|
-
nginx -t && nginx -s reload || rollback_failed=1
|
|
639
|
-
rm -rf -- "$transaction"
|
|
640
|
-
[[ "$rollback_failed" -eq 0 ]] || fail "public endpoint verification failed and old project traffic could not be restored"
|
|
641
|
-
fail "public endpoint verification failed; old project traffic restored"
|
|
642
|
-
fi
|
|
343
|
+
stop_services() { image_names; compose stop frontend backend; }
|
|
643
344
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
package_root="$DEPLOY_ROOT/$RELEASE_VERSION"
|
|
664
|
-
validate_uploaded_package "$package_root"
|
|
665
|
-
read_active
|
|
666
|
-
if [[ "$mode" == start ]]; then
|
|
667
|
-
[[ -z "$ACTIVE_VERSION" ]] || fail "an active deployment already exists; use replace"
|
|
668
|
-
target_color=blue
|
|
669
|
-
else
|
|
670
|
-
[[ -n "$ACTIVE_VERSION" ]] || fail "no active deployment exists; use start"
|
|
671
|
-
[[ "$RELEASE_VERSION" != "$ACTIVE_VERSION" ]] || fail "replace version must differ from active version"
|
|
672
|
-
target_color=$([[ "$ACTIVE_COLOR" == blue ]] && printf green || printf blue)
|
|
673
|
-
fi
|
|
674
|
-
mkdir -p "$STATE_DIR" "$NGINX_DIR" "$DEPLOY_ROOT/logs"
|
|
675
|
-
load_release_images "$RELEASE_VERSION"
|
|
676
|
-
up_pair "$target_color" "$RELEASE_VERSION"
|
|
677
|
-
verify_pair "$target_color" "$RELEASE_VERSION"
|
|
678
|
-
|
|
679
|
-
work="$(mktemp -d "$DEPLOY_ROOT/.traffic-candidate.XXXXXX")"
|
|
680
|
-
site_candidate="$work/site.conf"; upstream_candidate="$work/upstreams.conf"; active_candidate="$work/active.env"
|
|
681
|
-
render_site_candidate "$package_root/nginx/site.conf" "$site_candidate"
|
|
682
|
-
write_upstream_candidate "$target_color" "$upstream_candidate"
|
|
683
|
-
write_state_candidate "$active_candidate" "$target_color" "$RELEASE_VERSION"
|
|
684
|
-
if [[ -n "$ACTIVE_VERSION" ]]; then
|
|
685
|
-
previous_candidate="$work/previous.env"
|
|
686
|
-
write_state_candidate "$previous_candidate" "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
687
|
-
fi
|
|
688
|
-
trap 'rm -rf -- "${work:-}"' EXIT
|
|
689
|
-
commit_traffic_and_state "$site_candidate" "$upstream_candidate" "$active_candidate" "$previous_candidate" "$RELEASE_VERSION"
|
|
690
|
-
rm -rf -- "$work"
|
|
691
|
-
trap - EXIT
|
|
692
|
-
echo "active release: $RELEASE_VERSION ($target_color)"
|
|
693
|
-
return 0
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
restart_current() {
|
|
697
|
-
read_active
|
|
698
|
-
[[ -n "$ACTIVE_VERSION" ]] || fail "no active deployment"
|
|
699
|
-
validate_uploaded_package "$DEPLOY_ROOT/$ACTIVE_VERSION"
|
|
700
|
-
load_release_images "$ACTIVE_VERSION"
|
|
701
|
-
up_pair "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
702
|
-
verify_pair "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
stop_current() {
|
|
706
|
-
read_active
|
|
707
|
-
[[ -n "$ACTIVE_VERSION" ]] || fail "no active deployment"
|
|
708
|
-
compose_for "$ACTIVE_VERSION" stop "frontend-$ACTIVE_COLOR" "backend-$ACTIVE_COLOR"
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
status_current() {
|
|
712
|
-
read_active; read_previous
|
|
713
|
-
printf 'active_color=%s\nactive_version=%s\nprevious_color=%s\nprevious_version=%s\n' "$ACTIVE_COLOR" "$ACTIVE_VERSION" "$PREVIOUS_COLOR" "$PREVIOUS_VERSION"
|
|
714
|
-
if [[ -n "$ACTIVE_VERSION" ]]; then
|
|
715
|
-
compose_for "$ACTIVE_VERSION" ps
|
|
716
|
-
fi
|
|
345
|
+
server_menu() {
|
|
346
|
+
local choice status
|
|
347
|
+
initialize_server_context
|
|
348
|
+
printf '%s\n' '1. 运行或替换前端' '2. 运行或替换后端' '3. 运行或替换前后端' '4. 停止'
|
|
349
|
+
IFS= read -r choice || fail "failed to read deployment menu selection"
|
|
350
|
+
case "$choice" in
|
|
351
|
+
1) OPERATION=run-or-replace-frontend ;;
|
|
352
|
+
2) OPERATION=run-or-replace-backend ;;
|
|
353
|
+
3) OPERATION=run-or-replace-both ;;
|
|
354
|
+
4) OPERATION=stop-both ;;
|
|
355
|
+
*) OPERATION=invalid-selection; record_operation failure; fail "invalid deployment menu selection: $choice" ;;
|
|
356
|
+
esac
|
|
357
|
+
trap 'status=$?; if [[ $status -eq 0 ]]; then record_operation success; else record_operation failure; fi' EXIT
|
|
358
|
+
case "$choice" in
|
|
359
|
+
1) run_or_replace frontend ;;
|
|
360
|
+
2) run_or_replace backend ;;
|
|
361
|
+
3) run_or_replace both ;;
|
|
362
|
+
4) stop_services ;;
|
|
363
|
+
esac
|
|
717
364
|
}
|
|
718
365
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
while IFS= read -r value; do [[ -n "$value" ]] && container_ids+=("$value"); done <<< "$container_output"
|
|
724
|
-
((${#container_ids[@]} == 0)) || docker rm -f "${container_ids[@]}"
|
|
725
|
-
network_output="$(docker network ls -q --filter "label=com.docker.compose.project=$PROJECT_NAME")"
|
|
726
|
-
while IFS= read -r value; do [[ -n "$value" ]] && network_ids+=("$value"); done <<< "$network_output"
|
|
727
|
-
((${#network_ids[@]} == 0)) || docker network rm "${network_ids[@]}"
|
|
728
|
-
image_output="$(docker image ls --format '{{.Repository}}:{{.Tag}}' | awk -F: -v frontend="$FRONTEND_IMAGE_REPOSITORY" -v backend="$BACKEND_IMAGE_REPOSITORY" '($1 == frontend || $1 == backend) && $2 != "<none>" { print $0 }' | LC_ALL=C sort -u)"
|
|
729
|
-
while IFS= read -r value; do [[ -n "$value" ]] && image_references+=("$value"); done <<< "$image_output"
|
|
730
|
-
((${#image_references[@]} == 0)) || docker image rm -f "${image_references[@]}"
|
|
731
|
-
echo "removed only Docker resources for project: $PROJECT_NAME"
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
rollback_version() {
|
|
735
|
-
local requested="$1" work site_candidate upstream_candidate active_candidate previous_candidate
|
|
736
|
-
assert_version "$requested"
|
|
737
|
-
read_active; read_previous
|
|
738
|
-
[[ -n "$ACTIVE_VERSION" && -n "$PREVIOUS_VERSION" ]] || fail "active and previous deployment states are required"
|
|
739
|
-
[[ "$requested" == "$PREVIOUS_VERSION" ]] || fail "rollback target must equal previous version $PREVIOUS_VERSION"
|
|
740
|
-
verify_pair "$PREVIOUS_COLOR" "$PREVIOUS_VERSION"
|
|
741
|
-
work="$(mktemp -d "$DEPLOY_ROOT/.rollback-candidate.XXXXXX")"
|
|
742
|
-
site_candidate="$work/site.conf"; upstream_candidate="$work/upstreams.conf"; active_candidate="$work/active.env"; previous_candidate="$work/previous.env"
|
|
743
|
-
render_site_candidate "$DEPLOY_ROOT/$PREVIOUS_VERSION/nginx/site.conf" "$site_candidate"
|
|
744
|
-
write_upstream_candidate "$PREVIOUS_COLOR" "$upstream_candidate"
|
|
745
|
-
write_state_candidate "$active_candidate" "$PREVIOUS_COLOR" "$PREVIOUS_VERSION"
|
|
746
|
-
write_state_candidate "$previous_candidate" "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
747
|
-
trap 'rm -rf -- "${work:-}"' EXIT
|
|
748
|
-
commit_traffic_and_state "$site_candidate" "$upstream_candidate" "$active_candidate" "$previous_candidate" "$PREVIOUS_VERSION"
|
|
749
|
-
rm -rf -- "$work"
|
|
750
|
-
trap - EXIT
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
case "${1:-}" in
|
|
754
|
-
build) [[ "$#" -eq 1 ]] || usage; build_release ;;
|
|
755
|
-
start) [[ "$#" -eq 1 ]] || usage; initialize_server_context; deploy_release start ;;
|
|
756
|
-
replace) [[ "$#" -eq 1 ]] || usage; initialize_server_context; deploy_release replace ;;
|
|
757
|
-
restart) [[ "$#" -eq 1 ]] || usage; initialize_server_context; restart_current ;;
|
|
758
|
-
stop) [[ "$#" -eq 1 ]] || usage; initialize_server_context; stop_current ;;
|
|
759
|
-
remove) [[ "$#" -eq 1 ]] || usage; initialize_server_context; remove_project_docker ;;
|
|
760
|
-
status) [[ "$#" -eq 1 ]] || usage; initialize_server_context; status_current ;;
|
|
761
|
-
rollback) [[ "$#" -eq 2 ]] || usage; initialize_server_context; rollback_version "$2" ;;
|
|
762
|
-
*) usage ;;
|
|
763
|
-
esac
|
|
366
|
+
if [[ "$#" -eq 0 ]]; then server_menu
|
|
367
|
+
elif [[ "$#" -eq 1 && "$1" == build ]]; then build_release
|
|
368
|
+
else usage
|
|
369
|
+
fi
|