@wdyy/skills 0.1.4 → 0.1.6
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 +4 -4
- package/.well-known/skills/wdyy-api-standard/SKILL.md +3 -2
- package/.well-known/skills/wdyy-api-standard/reference/{api-/346/240/271/346/215/256/345/260/261/350/257/212/345/217/267/350/216/267/345/217/226/346/243/200/351/252/214/346/225/260/346/215/256.md → api-demo.md} +5 -3
- package/.well-known/skills/wdyy-api-standard/reference/api-sms.md +67 -0
- package/.well-known/skills/wdyy-deployment-standard/SKILL.md +34 -21
- package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +129 -7
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +159 -11
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +576 -38
- package/.well-known/skills/wdyy-deployment-standard/templates/Dockerfile.template +6 -1
- package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +588 -77
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +10 -8
- package/.well-known/skills/wdyy-deployment-standard/templates/dockerignore.template +15 -0
- package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +5 -4
- package/.well-known/skills/wdyy-logging-standard/SKILL.md +18 -21
- package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +8 -7
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.mjs +0 -19
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +71 -25
- package/.well-known/skills/wdyy-logging-standard/templates/frontend-error-report.template.ts +1 -6
- package/.well-known/skills/wdyy-logging-standard/templates/logger.template.ts +26 -25
- package/package.json +1 -1
|
@@ -1,29 +1,462 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
PREVIOUS_STATE="$STATE_DIR/previous.env"
|
|
17
|
-
INCOMING_DIR="$DEPLOY_ROOT/incoming"
|
|
18
|
-
RELEASES_DIR="$DEPLOY_ROOT/releases"
|
|
19
|
-
|
|
20
|
-
mkdir -p "$STATE_DIR" "$RELEASES_DIR"
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
|
|
5
|
+
SCRIPT_PATH="$SCRIPT_DIR/$(basename -- "${BASH_SOURCE[0]}")"
|
|
6
|
+
|
|
7
|
+
fail() {
|
|
8
|
+
echo "$*" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
echo "usage: ./deploy.sh build|start|stop|restart|status|rollback <version>" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
}
|
|
21
16
|
|
|
22
17
|
assert_version() {
|
|
23
|
-
[[ "${1:-}" =~ ^[0-9]{8}-[0-9]{3}$ ]] ||
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
[[ "${1:-}" =~ ^[0-9]{8}-[0-9]{3}$ ]] || fail "version must match YYYYMMDD-NNN"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
assert_port() {
|
|
22
|
+
local name="$1" value="$2"
|
|
23
|
+
[[ "$value" =~ ^[0-9]+$ ]] || fail "$name must be a numeric port"
|
|
24
|
+
(( value >= 1 && value <= 65535 )) || fail "$name must be between 1 and 65535"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
require_command() {
|
|
28
|
+
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
load_env_if_present() {
|
|
32
|
+
local file="$1"
|
|
33
|
+
if [[ -f "$file" ]]; then
|
|
34
|
+
set -a
|
|
35
|
+
# shellcheck disable=SC1090
|
|
36
|
+
source "$file"
|
|
37
|
+
set +a
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
load_required_env() {
|
|
42
|
+
local file="$1"
|
|
43
|
+
[[ -r "$file" ]] || fail "required production environment file is missing: $file"
|
|
44
|
+
set -a
|
|
45
|
+
# shellcheck disable=SC1090
|
|
46
|
+
source "$file"
|
|
47
|
+
set +a
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resolve_project_path() {
|
|
51
|
+
local project_root="$1" candidate="$2"
|
|
52
|
+
if [[ "$candidate" = /* ]]; then
|
|
53
|
+
printf '%s\n' "$candidate"
|
|
54
|
+
else
|
|
55
|
+
printf '%s/%s\n' "$project_root" "$candidate"
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
select_checksum_tool() {
|
|
60
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
61
|
+
CHECKSUM_TOOL=sha256sum
|
|
62
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
63
|
+
CHECKSUM_TOOL=shasum
|
|
64
|
+
else
|
|
65
|
+
fail "sha256sum or shasum is required"
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
checksum_value() {
|
|
70
|
+
local file="$1"
|
|
71
|
+
if [[ "$CHECKSUM_TOOL" == sha256sum ]]; then
|
|
72
|
+
sha256sum "$file" | awk '{print $1}'
|
|
73
|
+
else
|
|
74
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
release_ledger() {
|
|
79
|
+
node --input-type=module - "$@" <<'NODE'
|
|
80
|
+
import fs from 'node:fs';
|
|
81
|
+
|
|
82
|
+
const [mode, agentsPath, value, timestamp, outputPath] = process.argv.slice(2);
|
|
83
|
+
const heading = '## 发布记录';
|
|
84
|
+
const header = '| 版本号 | 构建时间 |';
|
|
85
|
+
const separator = '|---|---|';
|
|
86
|
+
const versionPattern = /^(\d{4})(\d{2})(\d{2})-(\d{3})$/;
|
|
87
|
+
const rowPattern = /^\| (\d{8}-\d{3}) \| (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{2}:\d{2}) \|$/;
|
|
88
|
+
|
|
89
|
+
function fail(message) {
|
|
90
|
+
throw new Error(message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validCalendarDate(dateText) {
|
|
94
|
+
const year = Number(dateText.slice(0, 4));
|
|
95
|
+
const month = Number(dateText.slice(4, 6));
|
|
96
|
+
const day = Number(dateText.slice(6, 8));
|
|
97
|
+
const value = new Date(Date.UTC(year, month - 1, day));
|
|
98
|
+
return value.getUTCFullYear() === year
|
|
99
|
+
&& value.getUTCMonth() === month - 1
|
|
100
|
+
&& value.getUTCDate() === day;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function inspectLedger(text) {
|
|
104
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
105
|
+
const headings = [];
|
|
106
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
107
|
+
if (lines[index] === heading) headings.push(index);
|
|
108
|
+
}
|
|
109
|
+
if (headings.length > 1) fail('AGENTS.md contains multiple release record sections');
|
|
110
|
+
if (headings.length === 0) return { lines, headingIndex: -1, endIndex: lines.length, versions: [] };
|
|
111
|
+
|
|
112
|
+
const headingIndex = headings[0];
|
|
113
|
+
let endIndex = lines.length;
|
|
114
|
+
for (let index = headingIndex + 1; index < lines.length; index += 1) {
|
|
115
|
+
if (/^## /.test(lines[index])) {
|
|
116
|
+
endIndex = index;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const section = lines.slice(headingIndex + 1, endIndex);
|
|
122
|
+
while (section[0] === '') section.shift();
|
|
123
|
+
while (section[section.length - 1] === '') section.pop();
|
|
124
|
+
if (section[0] !== header || section[1] !== separator) {
|
|
125
|
+
fail('AGENTS.md release record table must contain only version and build time columns');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const versions = [];
|
|
129
|
+
const unique = new Set();
|
|
130
|
+
for (const row of section.slice(2)) {
|
|
131
|
+
const match = row.match(rowPattern);
|
|
132
|
+
if (!match) fail(`invalid AGENTS.md release record row: ${row}`);
|
|
133
|
+
const versionMatch = match[1].match(versionPattern);
|
|
134
|
+
if (!versionMatch || !validCalendarDate(match[1].slice(0, 8))) {
|
|
135
|
+
fail(`invalid release version: ${match[1]}`);
|
|
136
|
+
}
|
|
137
|
+
const sequence = Number(versionMatch[4]);
|
|
138
|
+
if (sequence < 1 || sequence > 999) fail(`invalid release sequence: ${match[1]}`);
|
|
139
|
+
if (unique.has(match[1])) fail(`duplicate release version: ${match[1]}`);
|
|
140
|
+
unique.add(match[1]);
|
|
141
|
+
versions.push(match[1]);
|
|
142
|
+
}
|
|
143
|
+
return { lines, headingIndex, endIndex, versions };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function nextVersion(ledger, currentDate) {
|
|
147
|
+
if (!/^\d{8}$/.test(currentDate) || !validCalendarDate(currentDate)) {
|
|
148
|
+
fail(`invalid current release date: ${currentDate}`);
|
|
149
|
+
}
|
|
150
|
+
let maximum = 0;
|
|
151
|
+
for (const version of ledger.versions) {
|
|
152
|
+
if (version.startsWith(`${currentDate}-`)) {
|
|
153
|
+
maximum = Math.max(maximum, Number(version.slice(-3)));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (maximum >= 999) fail(`release sequence exhausted for ${currentDate}`);
|
|
157
|
+
return `${currentDate}-${String(maximum + 1).padStart(3, '0')}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const original = fs.readFileSync(agentsPath, 'utf8');
|
|
161
|
+
const ledger = inspectLedger(original);
|
|
162
|
+
|
|
163
|
+
if (mode === 'next') {
|
|
164
|
+
process.stdout.write(`${nextVersion(ledger, value)}\n`);
|
|
165
|
+
} else if (mode === 'prepare') {
|
|
166
|
+
const versionMatch = value.match(versionPattern);
|
|
167
|
+
if (!versionMatch || !validCalendarDate(value.slice(0, 8))) fail(`invalid release version: ${value}`);
|
|
168
|
+
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{2}:\d{2}$/.test(timestamp)) {
|
|
169
|
+
fail(`invalid release build time: ${timestamp}`);
|
|
170
|
+
}
|
|
171
|
+
const expected = nextVersion(ledger, value.slice(0, 8));
|
|
172
|
+
if (value !== expected) fail(`release ledger changed: expected ${expected}, got ${value}`);
|
|
173
|
+
const row = `| ${value} | ${timestamp} |`;
|
|
174
|
+
let updated;
|
|
175
|
+
if (ledger.headingIndex === -1) {
|
|
176
|
+
const separatorText = original.length === 0 ? '' : original.endsWith('\n') ? '\n' : '\n\n';
|
|
177
|
+
updated = `${original}${separatorText}${heading}\n\n${header}\n${separator}\n${row}\n`;
|
|
178
|
+
} else {
|
|
179
|
+
const lines = [...ledger.lines];
|
|
180
|
+
let insertAt = ledger.endIndex;
|
|
181
|
+
while (insertAt > ledger.headingIndex + 1 && lines[insertAt - 1] === '') insertAt -= 1;
|
|
182
|
+
lines.splice(insertAt, 0, row);
|
|
183
|
+
updated = lines.join('\n');
|
|
26
184
|
}
|
|
185
|
+
const modeBits = fs.statSync(agentsPath).mode;
|
|
186
|
+
fs.writeFileSync(outputPath, updated, { mode: modeBits });
|
|
187
|
+
} else {
|
|
188
|
+
fail(`unknown release ledger mode: ${mode}`);
|
|
189
|
+
}
|
|
190
|
+
NODE
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
create_manifest() {
|
|
194
|
+
local version_root="$1" file relative checksum
|
|
195
|
+
(
|
|
196
|
+
cd "$version_root"
|
|
197
|
+
: > manifest.sha256
|
|
198
|
+
find . -type f ! -name manifest.sha256 -print | LC_ALL=C sort |
|
|
199
|
+
while IFS= read -r file; do
|
|
200
|
+
relative="${file#./}"
|
|
201
|
+
checksum="$(checksum_value "$file")"
|
|
202
|
+
printf '%s %s\n' "$checksum" "$relative"
|
|
203
|
+
done > manifest.sha256
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
verify_manifest() {
|
|
208
|
+
local version_root="$1"
|
|
209
|
+
local manifest="$version_root/manifest.sha256"
|
|
210
|
+
local actual_paths declared_paths result line checksum relative actual
|
|
211
|
+
[[ -s "$manifest" ]] || fail "missing or empty manifest.sha256"
|
|
212
|
+
actual_paths="$(mktemp)"
|
|
213
|
+
declared_paths="$(mktemp)"
|
|
214
|
+
result=0
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
cd "$version_root"
|
|
218
|
+
find . -type f ! -name manifest.sha256 -print |
|
|
219
|
+
sed 's|^\./||' |
|
|
220
|
+
LC_ALL=C sort > "$actual_paths"
|
|
221
|
+
while IFS= read -r line; do
|
|
222
|
+
checksum="${line%% *}"
|
|
223
|
+
relative="${line#* }"
|
|
224
|
+
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ || "$relative" == "$line" || "$relative" == /* || "$relative" == ../* || "$relative" == *"/../"* ]]; then
|
|
225
|
+
echo "invalid manifest entry: $line" >&2
|
|
226
|
+
result=1
|
|
227
|
+
continue
|
|
228
|
+
fi
|
|
229
|
+
printf '%s\n' "$relative" >> "$declared_paths"
|
|
230
|
+
[[ -f "$relative" ]] || {
|
|
231
|
+
echo "manifest file is missing: $relative" >&2
|
|
232
|
+
result=1
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
actual="$(checksum_value "$relative")"
|
|
236
|
+
if [[ "$actual" != "$checksum" ]]; then
|
|
237
|
+
echo "checksum mismatch: $relative" >&2
|
|
238
|
+
result=1
|
|
239
|
+
fi
|
|
240
|
+
done < manifest.sha256
|
|
241
|
+
LC_ALL=C sort -o "$declared_paths" "$declared_paths"
|
|
242
|
+
if ! cmp -s "$actual_paths" "$declared_paths"; then
|
|
243
|
+
echo "manifest does not cover the complete release package" >&2
|
|
244
|
+
result=1
|
|
245
|
+
fi
|
|
246
|
+
exit "$result"
|
|
247
|
+
); then
|
|
248
|
+
result=0
|
|
249
|
+
else
|
|
250
|
+
result=$?
|
|
251
|
+
fi
|
|
252
|
+
rm -f -- "$actual_paths" "$declared_paths"
|
|
253
|
+
[[ "$result" -eq 0 ]] || fail "release manifest validation failed"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
reject_secret_files() {
|
|
257
|
+
local root="$1" file base
|
|
258
|
+
if find "$root" -type l -print -quit | grep -q .; then
|
|
259
|
+
fail "symbolic links must not be packaged"
|
|
260
|
+
fi
|
|
261
|
+
while IFS= read -r file; do
|
|
262
|
+
base="${file##*/}"
|
|
263
|
+
case "$base" in
|
|
264
|
+
.env|.env.*|*.pem|*.key|id_rsa|id_ed25519|credentials|credentials.*)
|
|
265
|
+
fail "production secret file must not be packaged: ${file#"$root"/}"
|
|
266
|
+
;;
|
|
267
|
+
esac
|
|
268
|
+
done < <(find "$root" -type f -print)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
validate_dockerignore() {
|
|
272
|
+
local file="$1" pattern
|
|
273
|
+
[[ -f "$file" ]] || fail "required Docker ignore file is missing: $file"
|
|
274
|
+
for pattern in \
|
|
275
|
+
'.env' \
|
|
276
|
+
'.env.*' \
|
|
277
|
+
'*.pem' \
|
|
278
|
+
'*.key' \
|
|
279
|
+
'id_rsa' \
|
|
280
|
+
'id_ed25519' \
|
|
281
|
+
'credentials' \
|
|
282
|
+
'credentials.*' \
|
|
283
|
+
'deploy/' \
|
|
284
|
+
'logs/' \
|
|
285
|
+
'.git/' \
|
|
286
|
+
'node_modules/' \
|
|
287
|
+
'venv/' \
|
|
288
|
+
'.pnpm-store/'; do
|
|
289
|
+
grep -Fxq "$pattern" "$file" || fail "Docker ignore file is missing required pattern: $pattern"
|
|
290
|
+
done
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
validate_local_package() {
|
|
294
|
+
local output_root="$1" version="$2"
|
|
295
|
+
local version_root="$output_root/$version"
|
|
296
|
+
[[ -x "$output_root/deploy.sh" ]] || fail "generated deploy.sh is missing or not executable"
|
|
297
|
+
[[ "$(cat "$output_root/release.env")" == "RELEASE_VERSION=$version" ]] ||
|
|
298
|
+
fail "release.env does not point to the generated version"
|
|
299
|
+
[[ -f "$version_root/frontend.tar.gz" ]] || fail "missing frontend.tar.gz"
|
|
300
|
+
[[ -f "$version_root/backend-image.tar" ]] || fail "missing backend-image.tar"
|
|
301
|
+
[[ -d "$version_root/database/migrations" ]] || fail "missing database/migrations"
|
|
302
|
+
[[ -x "$version_root/scripts/apply-migrations.sh" ]] || fail "missing executable migration runner"
|
|
303
|
+
[[ -f "$version_root/docker/docker-compose.blue-green.yml" ]] || fail "missing blue-green compose file"
|
|
304
|
+
[[ -f "$version_root/nginx/site.conf" ]] || fail "missing Nginx site configuration"
|
|
305
|
+
grep -Fq '../../logs:/app/logs' "$version_root/docker/docker-compose.blue-green.yml" ||
|
|
306
|
+
fail "compose does not mount deployment-root logs"
|
|
307
|
+
grep -Fq '__DEPLOY_ROOT__' "$version_root/nginx/site.conf" ||
|
|
308
|
+
fail "Nginx configuration does not contain the deployment-root placeholder"
|
|
309
|
+
grep -Fq '__BACKEND_PROXY_URL__' "$version_root/nginx/site.conf" ||
|
|
310
|
+
fail "Nginx configuration does not contain the backend proxy URL placeholder"
|
|
311
|
+
reject_secret_files "$output_root"
|
|
312
|
+
verify_manifest "$version_root"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
build_release() {
|
|
316
|
+
local project_root="$SCRIPT_DIR" agents_file="$SCRIPT_DIR/AGENTS.md"
|
|
317
|
+
local today version build_time raw_time image staging_parent output_root version_root agents_prepared
|
|
318
|
+
local frontend_dist backend_dockerfile migrations_source migration_runner_source
|
|
319
|
+
local deploy_config_dir compose_source nginx_source dockerignore_file
|
|
320
|
+
|
|
321
|
+
[[ -r "$agents_file" && -w "$agents_file" ]] || fail "project AGENTS.md must exist and be readable and writable"
|
|
322
|
+
load_env_if_present "$project_root/.env"
|
|
323
|
+
: "${PROJECT_NAME:?PROJECT_NAME is required in the environment or project .env}"
|
|
324
|
+
: "${BACKEND_CONTAINER_PORT:?BACKEND_CONTAINER_PORT is required in the environment or project .env}"
|
|
325
|
+
assert_port BACKEND_CONTAINER_PORT "$BACKEND_CONTAINER_PORT"
|
|
326
|
+
|
|
327
|
+
frontend_dist="$(resolve_project_path "$project_root" "${FRONTEND_DIST_DIR:-src/frontend/dist}")"
|
|
328
|
+
backend_dockerfile="$(resolve_project_path "$project_root" "${BACKEND_DOCKERFILE:-src/backend/Dockerfile}")"
|
|
329
|
+
dockerignore_file="$(resolve_project_path "$project_root" "${DOCKERIGNORE_FILE:-.dockerignore}")"
|
|
330
|
+
migrations_source="$(resolve_project_path "$project_root" "${MIGRATIONS_SOURCE:-database/migrations}")"
|
|
331
|
+
migration_runner_source="$(resolve_project_path "$project_root" "${MIGRATION_RUNNER_SOURCE:-scripts/apply-migrations.sh}")"
|
|
332
|
+
deploy_config_dir="$(resolve_project_path "$project_root" "${DEPLOY_CONFIG_DIR:-scripts/deployment}")"
|
|
333
|
+
compose_source="$(resolve_project_path "$project_root" "${COMPOSE_SOURCE:-${deploy_config_dir#$project_root/}/docker-compose.blue-green.yml}")"
|
|
334
|
+
nginx_source="$(resolve_project_path "$project_root" "${NGINX_SOURCE:-${deploy_config_dir#$project_root/}/nginx-site.conf}")"
|
|
335
|
+
BACKEND_IMAGE_PREFIX="${BACKEND_IMAGE_PREFIX:-$PROJECT_NAME-backend}"
|
|
336
|
+
|
|
337
|
+
for command in node pnpm docker tar install find sort cmp awk sed grep; do
|
|
338
|
+
require_command "$command"
|
|
339
|
+
done
|
|
340
|
+
select_checksum_tool
|
|
341
|
+
[[ -f "$backend_dockerfile" ]] || fail "backend Dockerfile not found: $backend_dockerfile"
|
|
342
|
+
validate_dockerignore "$dockerignore_file"
|
|
343
|
+
[[ -d "$migrations_source" ]] || fail "migration directory not found: $migrations_source"
|
|
344
|
+
[[ -x "$migration_runner_source" ]] || fail "migration runner is missing or not executable: $migration_runner_source"
|
|
345
|
+
[[ -f "$compose_source" ]] || fail "compose source not found: $compose_source"
|
|
346
|
+
[[ -f "$nginx_source" ]] || fail "Nginx source not found: $nginx_source"
|
|
347
|
+
|
|
348
|
+
today="$(date +%Y%m%d)"
|
|
349
|
+
version="$(release_ledger next "$agents_file" "$today")"
|
|
350
|
+
assert_version "$version"
|
|
351
|
+
image="$BACKEND_IMAGE_PREFIX:$version"
|
|
352
|
+
|
|
353
|
+
echo "release gate: pnpm test"
|
|
354
|
+
pnpm test
|
|
355
|
+
echo "release gate: pnpm lint"
|
|
356
|
+
pnpm lint
|
|
357
|
+
echo "release gate: pnpm typecheck"
|
|
358
|
+
pnpm typecheck
|
|
359
|
+
echo "release gate: pnpm build"
|
|
360
|
+
RELEASE_VERSION="$version" VITE_RELEASE_BASE="/releases/$version/" pnpm build
|
|
361
|
+
|
|
362
|
+
[[ -f "$frontend_dist/index.html" ]] || fail "frontend build missing index.html: $frontend_dist"
|
|
363
|
+
grep -Fq "/releases/$version/" "$frontend_dist/index.html" ||
|
|
364
|
+
fail "frontend assets are not built with /releases/$version/"
|
|
365
|
+
|
|
366
|
+
echo "release gate: docker build"
|
|
367
|
+
docker build \
|
|
368
|
+
--build-arg "RELEASE_VERSION=$version" \
|
|
369
|
+
--build-arg "BACKEND_CONTAINER_PORT=$BACKEND_CONTAINER_PORT" \
|
|
370
|
+
--tag "$image" \
|
|
371
|
+
--file "$backend_dockerfile" \
|
|
372
|
+
"$project_root"
|
|
373
|
+
docker image inspect "$image" >/dev/null
|
|
374
|
+
|
|
375
|
+
staging_parent="$(mktemp -d "$project_root/.deploy-build.XXXXXX")"
|
|
376
|
+
output_root="$staging_parent/deploy"
|
|
377
|
+
version_root="$output_root/$version"
|
|
378
|
+
agents_prepared="$project_root/.AGENTS.md.release.$$.tmp"
|
|
379
|
+
trap 'rm -rf -- "${staging_parent:-}"; rm -f -- "${agents_prepared:-}"' EXIT
|
|
380
|
+
|
|
381
|
+
mkdir -p \
|
|
382
|
+
"$version_root/database" \
|
|
383
|
+
"$version_root/scripts" \
|
|
384
|
+
"$version_root/docker" \
|
|
385
|
+
"$version_root/nginx"
|
|
386
|
+
install -m 0755 "$SCRIPT_PATH" "$output_root/deploy.sh"
|
|
387
|
+
printf 'RELEASE_VERSION=%s\n' "$version" > "$output_root/release.env"
|
|
388
|
+
tar -czf "$version_root/frontend.tar.gz" -C "$frontend_dist" .
|
|
389
|
+
docker save -o "$version_root/backend-image.tar" "$image"
|
|
390
|
+
cp -R "$migrations_source" "$version_root/database/migrations"
|
|
391
|
+
install -m 0755 "$migration_runner_source" "$version_root/scripts/apply-migrations.sh"
|
|
392
|
+
install -m 0644 "$compose_source" "$version_root/docker/docker-compose.blue-green.yml"
|
|
393
|
+
install -m 0644 "$nginx_source" "$version_root/nginx/site.conf"
|
|
394
|
+
create_manifest "$version_root"
|
|
395
|
+
validate_local_package "$output_root" "$version"
|
|
396
|
+
|
|
397
|
+
raw_time="$(date '+%Y-%m-%d %H:%M:%S %z')"
|
|
398
|
+
build_time="${raw_time%?????}${raw_time: -5:3}:${raw_time: -2}"
|
|
399
|
+
release_ledger prepare "$agents_file" "$version" "$build_time" "$agents_prepared"
|
|
400
|
+
|
|
401
|
+
[[ "$project_root/deploy" == "$SCRIPT_DIR/deploy" ]] || fail "refusing to replace an unexpected deploy directory"
|
|
402
|
+
rm -rf -- "$project_root/deploy"
|
|
403
|
+
mv "$output_root" "$project_root/deploy"
|
|
404
|
+
if ! mv "$agents_prepared" "$agents_file"; then
|
|
405
|
+
rm -rf -- "$project_root/deploy"
|
|
406
|
+
fail "failed to write AGENTS.md release record"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
trap - EXIT
|
|
410
|
+
rm -rf -- "$staging_parent"
|
|
411
|
+
echo "release package created: deploy/$version"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
read_release_pointer() {
|
|
415
|
+
local file="$DEPLOY_ROOT/release.env" line
|
|
416
|
+
[[ -f "$file" ]] || fail "missing release.env"
|
|
417
|
+
[[ "$(wc -l < "$file" | tr -d ' ')" == 1 ]] || fail "release.env must contain exactly one line"
|
|
418
|
+
line="$(cat "$file")"
|
|
419
|
+
[[ "$line" =~ ^RELEASE_VERSION=([0-9]{8}-[0-9]{3})$ ]] || fail "invalid release.env"
|
|
420
|
+
RELEASE_VERSION="${BASH_REMATCH[1]}"
|
|
421
|
+
assert_version "$RELEASE_VERSION"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
initialize_server_context() {
|
|
425
|
+
DEPLOY_ROOT="$SCRIPT_DIR"
|
|
426
|
+
load_required_env "$DEPLOY_ROOT/.env.production"
|
|
427
|
+
: "${PROJECT_NAME:?PROJECT_NAME is required in .env.production}"
|
|
428
|
+
: "${DATABASE_URL:?DATABASE_URL is required in .env.production}"
|
|
429
|
+
: "${BACKEND_BIND_IP:?BACKEND_BIND_IP is required in .env.production}"
|
|
430
|
+
: "${BACKEND_LISTEN_HOST:?BACKEND_LISTEN_HOST is required in .env.production}"
|
|
431
|
+
: "${BACKEND_CONTAINER_PORT:?BACKEND_CONTAINER_PORT is required in .env.production}"
|
|
432
|
+
: "${BACKEND_CONTAINER_HEALTH_URL:?BACKEND_CONTAINER_HEALTH_URL is required in .env.production}"
|
|
433
|
+
: "${BLUE_PORT:?BLUE_PORT is required in .env.production}"
|
|
434
|
+
: "${GREEN_PORT:?GREEN_PORT is required in .env.production}"
|
|
435
|
+
: "${BLUE_HEALTH_URL:?BLUE_HEALTH_URL is required in .env.production}"
|
|
436
|
+
: "${GREEN_HEALTH_URL:?GREEN_HEALTH_URL is required in .env.production}"
|
|
437
|
+
: "${BLUE_VERSION_URL:?BLUE_VERSION_URL is required in .env.production}"
|
|
438
|
+
: "${GREEN_VERSION_URL:?GREEN_VERSION_URL is required in .env.production}"
|
|
439
|
+
: "${BLUE_UPSTREAM:?BLUE_UPSTREAM is required in .env.production}"
|
|
440
|
+
: "${GREEN_UPSTREAM:?GREEN_UPSTREAM is required in .env.production}"
|
|
441
|
+
: "${BACKEND_PROXY_URL:?BACKEND_PROXY_URL is required in .env.production}"
|
|
442
|
+
if [[ ! "$BACKEND_PROXY_URL" =~ ^https?://([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
|
|
443
|
+
fail "BACKEND_PROXY_URL must be an http(s) URL containing only a valid Nginx upstream name"
|
|
444
|
+
fi
|
|
445
|
+
BACKEND_UPSTREAM_NAME="${BASH_REMATCH[1]}"
|
|
446
|
+
BACKEND_IMAGE_PREFIX="${BACKEND_IMAGE_PREFIX:-$PROJECT_NAME-backend}"
|
|
447
|
+
assert_port BACKEND_CONTAINER_PORT "$BACKEND_CONTAINER_PORT"
|
|
448
|
+
assert_port BLUE_PORT "$BLUE_PORT"
|
|
449
|
+
assert_port GREEN_PORT "$GREEN_PORT"
|
|
450
|
+
STATE_DIR="$DEPLOY_ROOT/state"
|
|
451
|
+
ACTIVE_STATE="$STATE_DIR/active.env"
|
|
452
|
+
PREVIOUS_STATE="$STATE_DIR/previous.env"
|
|
453
|
+
RELEASES_DIR="$DEPLOY_ROOT/releases"
|
|
454
|
+
UPSTREAM_FILE="$DEPLOY_ROOT/nginx/backend-active.conf"
|
|
455
|
+
NGINX_SITE_FILE="$DEPLOY_ROOT/nginx/site.conf"
|
|
456
|
+
for command in docker curl nginx psql tar find sort cmp awk sed grep; do
|
|
457
|
+
require_command "$command"
|
|
458
|
+
done
|
|
459
|
+
select_checksum_tool
|
|
27
460
|
}
|
|
28
461
|
|
|
29
462
|
read_active() {
|
|
@@ -32,18 +465,46 @@ read_active() {
|
|
|
32
465
|
if [[ -f "$ACTIVE_STATE" ]]; then
|
|
33
466
|
# shellcheck disable=SC1090
|
|
34
467
|
source "$ACTIVE_STATE"
|
|
468
|
+
[[ "$ACTIVE_COLOR" == blue || "$ACTIVE_COLOR" == green ]] || fail "invalid active color state"
|
|
469
|
+
assert_version "$ACTIVE_VERSION"
|
|
35
470
|
fi
|
|
36
471
|
}
|
|
37
472
|
|
|
38
|
-
|
|
39
|
-
[[ "$1" == blue ]] && printf '%s' "$
|
|
473
|
+
upstream_for() {
|
|
474
|
+
[[ "$1" == blue ]] && printf '%s' "$BLUE_UPSTREAM" || printf '%s' "$GREEN_UPSTREAM"
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
compose_file_for() {
|
|
478
|
+
local version="$1"
|
|
479
|
+
assert_version "$version"
|
|
480
|
+
printf '%s/%s/docker/docker-compose.blue-green.yml\n' "$DEPLOY_ROOT" "$version"
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
render_nginx_site() {
|
|
484
|
+
local source="$1" escaped_root escaped_proxy_url
|
|
485
|
+
grep -Fq '__DEPLOY_ROOT__' "$source" || fail "Nginx package is missing __DEPLOY_ROOT__ placeholder"
|
|
486
|
+
grep -Fq '__BACKEND_PROXY_URL__' "$source" ||
|
|
487
|
+
fail "Nginx package is missing __BACKEND_PROXY_URL__ placeholder"
|
|
488
|
+
escaped_root="${DEPLOY_ROOT//\\/\\\\}"
|
|
489
|
+
escaped_root="${escaped_root//&/\\&}"
|
|
490
|
+
escaped_root="${escaped_root//|/\\|}"
|
|
491
|
+
escaped_proxy_url="${BACKEND_PROXY_URL//\\/\\\\}"
|
|
492
|
+
escaped_proxy_url="${escaped_proxy_url//&/\\&}"
|
|
493
|
+
escaped_proxy_url="${escaped_proxy_url//|/\\|}"
|
|
494
|
+
mkdir -p "$DEPLOY_ROOT/nginx"
|
|
495
|
+
sed \
|
|
496
|
+
-e "s|__DEPLOY_ROOT__|$escaped_root|g" \
|
|
497
|
+
-e "s|__BACKEND_PROXY_URL__|$escaped_proxy_url|g" \
|
|
498
|
+
"$source" > "$NGINX_SITE_FILE.new"
|
|
499
|
+
mv "$NGINX_SITE_FILE.new" "$NGINX_SITE_FILE"
|
|
40
500
|
}
|
|
41
501
|
|
|
42
502
|
switch_traffic() {
|
|
43
|
-
local color="$1" version="$2"
|
|
44
|
-
|
|
503
|
+
local color="$1" version="$2" upstream backup
|
|
504
|
+
upstream="$(upstream_for "$color")"
|
|
45
505
|
backup="$UPSTREAM_FILE.previous"
|
|
46
|
-
printf 'upstream
|
|
506
|
+
printf 'upstream %s {\n server %s;\n keepalive 32;\n}\n' \
|
|
507
|
+
"$BACKEND_UPSTREAM_NAME" "$upstream" > "$UPSTREAM_FILE.new"
|
|
47
508
|
[[ -f "$UPSTREAM_FILE" ]] && cp "$UPSTREAM_FILE" "$backup"
|
|
48
509
|
mv -f "$UPSTREAM_FILE.new" "$UPSTREAM_FILE"
|
|
49
510
|
if ! nginx -t || ! nginx -s reload; then
|
|
@@ -61,82 +522,124 @@ switch_traffic() {
|
|
|
61
522
|
}
|
|
62
523
|
|
|
63
524
|
verify_backend() {
|
|
64
|
-
local color="$1" version="$2"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
525
|
+
local color="$1" version="$2" health_url version_url actual
|
|
526
|
+
if [[ "$color" == blue ]]; then
|
|
527
|
+
health_url="$BLUE_HEALTH_URL"
|
|
528
|
+
version_url="$BLUE_VERSION_URL"
|
|
529
|
+
else
|
|
530
|
+
health_url="$GREEN_HEALTH_URL"
|
|
531
|
+
version_url="$GREEN_VERSION_URL"
|
|
532
|
+
fi
|
|
68
533
|
HEALTH_URL="$health_url"
|
|
69
534
|
VERSION_URL="$version_url"
|
|
70
|
-
curl --fail --silent --show-error --retry 12 --retry-delay 2 "$HEALTH_URL"
|
|
535
|
+
curl --fail --silent --show-error --retry 12 --retry-delay 2 --retry-connrefused "$HEALTH_URL"
|
|
71
536
|
actual="$(curl --fail --silent --show-error "$VERSION_URL")"
|
|
72
|
-
[[ "$actual" == "$version" ]] ||
|
|
73
|
-
echo "backend version mismatch: expected $version, got $actual" >&2
|
|
74
|
-
exit 1
|
|
75
|
-
}
|
|
537
|
+
[[ "$actual" == "$version" ]] || fail "backend version mismatch: expected $version, got $actual"
|
|
76
538
|
}
|
|
77
539
|
|
|
78
|
-
|
|
79
|
-
local
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
[[ -
|
|
84
|
-
[[ -
|
|
85
|
-
[[ -
|
|
540
|
+
validate_uploaded_package() {
|
|
541
|
+
local package_root="$1"
|
|
542
|
+
select_checksum_tool
|
|
543
|
+
[[ -f "$package_root/frontend.tar.gz" ]] || fail "missing frontend.tar.gz"
|
|
544
|
+
[[ -f "$package_root/backend-image.tar" ]] || fail "missing backend-image.tar"
|
|
545
|
+
[[ -d "$package_root/database/migrations" ]] || fail "missing database/migrations"
|
|
546
|
+
[[ -x "$package_root/scripts/apply-migrations.sh" ]] || fail "missing executable migration runner"
|
|
547
|
+
[[ -f "$package_root/docker/docker-compose.blue-green.yml" ]] || fail "missing blue-green compose file"
|
|
548
|
+
[[ -f "$package_root/nginx/site.conf" ]] || fail "missing Nginx site configuration"
|
|
549
|
+
reject_secret_files "$package_root"
|
|
550
|
+
verify_manifest "$package_root"
|
|
551
|
+
if tar -tzf "$package_root/frontend.tar.gz" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
|
|
552
|
+
fail "frontend archive contains an unsafe path"
|
|
553
|
+
fi
|
|
554
|
+
}
|
|
86
555
|
|
|
87
|
-
|
|
556
|
+
start_release() {
|
|
557
|
+
local package_root release_dir target_color image compose_file
|
|
558
|
+
initialize_server_context
|
|
559
|
+
read_release_pointer
|
|
560
|
+
package_root="$DEPLOY_ROOT/$RELEASE_VERSION"
|
|
561
|
+
validate_uploaded_package "$package_root"
|
|
562
|
+
mkdir -p "$STATE_DIR" "$RELEASES_DIR" "$DEPLOY_ROOT/logs" "$DEPLOY_ROOT/nginx"
|
|
563
|
+
render_nginx_site "$package_root/nginx/site.conf"
|
|
564
|
+
|
|
565
|
+
release_dir="$RELEASES_DIR/$RELEASE_VERSION"
|
|
566
|
+
rm -rf -- "$release_dir"
|
|
88
567
|
mkdir -p "$release_dir"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
echo "frontend assets are not built with the versioned release URL" >&2
|
|
97
|
-
exit 1
|
|
98
|
-
}
|
|
99
|
-
docker load -i "$source_dir/backend-image.tar"
|
|
100
|
-
image="$BACKEND_IMAGE_PREFIX:$version"
|
|
568
|
+
tar -xzf "$package_root/frontend.tar.gz" -C "$release_dir"
|
|
569
|
+
[[ -f "$release_dir/index.html" ]] || fail "frontend release missing index.html"
|
|
570
|
+
grep -Fq "/releases/$RELEASE_VERSION/" "$release_dir/index.html" ||
|
|
571
|
+
fail "frontend assets are not built with the versioned release URL"
|
|
572
|
+
|
|
573
|
+
docker load -i "$package_root/backend-image.tar"
|
|
574
|
+
image="$BACKEND_IMAGE_PREFIX:$RELEASE_VERSION"
|
|
101
575
|
docker image inspect "$image" >/dev/null
|
|
102
|
-
MIGRATIONS_DIR="$
|
|
576
|
+
MIGRATIONS_DIR="$package_root/database/migrations" \
|
|
577
|
+
DATABASE_URL="$DATABASE_URL" \
|
|
578
|
+
"$package_root/scripts/apply-migrations.sh"
|
|
103
579
|
|
|
104
580
|
read_active
|
|
105
581
|
target_color=$([[ "$ACTIVE_COLOR" == blue ]] && echo green || echo blue)
|
|
582
|
+
compose_file="$(compose_file_for "$RELEASE_VERSION")"
|
|
106
583
|
if [[ "$target_color" == blue ]]; then
|
|
107
|
-
BACKEND_BLUE_IMAGE="$image" docker compose -
|
|
584
|
+
BACKEND_BLUE_IMAGE="$image" docker compose --project-name "$PROJECT_NAME" -f "$compose_file" \
|
|
585
|
+
up -d --force-recreate backend-blue
|
|
108
586
|
else
|
|
109
|
-
BACKEND_GREEN_IMAGE="$image" docker compose -
|
|
587
|
+
BACKEND_GREEN_IMAGE="$image" docker compose --project-name "$PROJECT_NAME" -f "$compose_file" \
|
|
588
|
+
up -d --force-recreate backend-green
|
|
110
589
|
fi
|
|
111
|
-
verify_backend "$target_color" "$
|
|
590
|
+
verify_backend "$target_color" "$RELEASE_VERSION"
|
|
112
591
|
[[ -f "$ACTIVE_STATE" ]] && cp "$ACTIVE_STATE" "$PREVIOUS_STATE"
|
|
113
|
-
switch_traffic "$target_color" "$
|
|
114
|
-
printf 'ACTIVE_COLOR=%q\nACTIVE_VERSION=%q\n' "$target_color" "$
|
|
592
|
+
switch_traffic "$target_color" "$RELEASE_VERSION"
|
|
593
|
+
printf 'ACTIVE_COLOR=%q\nACTIVE_VERSION=%q\n' "$target_color" "$RELEASE_VERSION" > "$ACTIVE_STATE"
|
|
115
594
|
}
|
|
116
595
|
|
|
117
596
|
restart_current() {
|
|
597
|
+
local target_color image compose_file
|
|
118
598
|
read_active
|
|
119
|
-
[[ -n "$ACTIVE_VERSION" ]] ||
|
|
120
|
-
local target_color image
|
|
599
|
+
[[ -n "$ACTIVE_VERSION" ]] || fail "no active version"
|
|
121
600
|
target_color=$([[ "$ACTIVE_COLOR" == blue ]] && echo green || echo blue)
|
|
122
601
|
image="$BACKEND_IMAGE_PREFIX:$ACTIVE_VERSION"
|
|
602
|
+
compose_file="$(compose_file_for "$ACTIVE_VERSION")"
|
|
603
|
+
[[ -f "$compose_file" ]] || fail "active version compose file is missing: $compose_file"
|
|
123
604
|
if [[ "$target_color" == blue ]]; then
|
|
124
|
-
BACKEND_BLUE_IMAGE="$image" docker compose -
|
|
605
|
+
BACKEND_BLUE_IMAGE="$image" docker compose --project-name "$PROJECT_NAME" -f "$compose_file" \
|
|
606
|
+
up -d --force-recreate backend-blue
|
|
125
607
|
else
|
|
126
|
-
BACKEND_GREEN_IMAGE="$image" docker compose -
|
|
608
|
+
BACKEND_GREEN_IMAGE="$image" docker compose --project-name "$PROJECT_NAME" -f "$compose_file" \
|
|
609
|
+
up -d --force-recreate backend-green
|
|
127
610
|
fi
|
|
128
611
|
verify_backend "$target_color" "$ACTIVE_VERSION"
|
|
129
612
|
switch_traffic "$target_color" "$ACTIVE_VERSION"
|
|
130
613
|
printf 'ACTIVE_COLOR=%q\nACTIVE_VERSION=%q\n' "$target_color" "$ACTIVE_VERSION" > "$ACTIVE_STATE"
|
|
131
614
|
}
|
|
132
615
|
|
|
616
|
+
stop_current() {
|
|
617
|
+
local compose_file
|
|
618
|
+
read_active
|
|
619
|
+
[[ -n "$ACTIVE_VERSION" ]] || fail "no active version"
|
|
620
|
+
compose_file="$(compose_file_for "$ACTIVE_VERSION")"
|
|
621
|
+
docker compose --project-name "$PROJECT_NAME" -f "$compose_file" down
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
status_current() {
|
|
625
|
+
local compose_file
|
|
626
|
+
read_active
|
|
627
|
+
printf 'color=%s\nversion=%s\n' "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
628
|
+
[[ -n "$ACTIVE_VERSION" ]] || return 0
|
|
629
|
+
compose_file="$(compose_file_for "$ACTIVE_VERSION")"
|
|
630
|
+
docker compose --project-name "$PROJECT_NAME" -f "$compose_file" ps
|
|
631
|
+
}
|
|
632
|
+
|
|
133
633
|
rollback_version() {
|
|
134
634
|
local version="$1"
|
|
135
635
|
assert_version "$version"
|
|
136
|
-
[[ -f "$PREVIOUS_STATE" ]] ||
|
|
636
|
+
[[ -f "$PREVIOUS_STATE" ]] || fail "no previous deployment"
|
|
137
637
|
# shellcheck disable=SC1090
|
|
138
638
|
source "$PREVIOUS_STATE"
|
|
139
|
-
[[ "$
|
|
639
|
+
[[ "$ACTIVE_COLOR" == blue || "$ACTIVE_COLOR" == green ]] || fail "invalid previous color state"
|
|
640
|
+
assert_version "$ACTIVE_VERSION"
|
|
641
|
+
[[ "$ACTIVE_VERSION" == "$version" ]] || fail "rollback target is not the retained previous version"
|
|
642
|
+
[[ -d "$RELEASES_DIR/$ACTIVE_VERSION" ]] || fail "previous frontend release is missing"
|
|
140
643
|
verify_backend "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
141
644
|
switch_traffic "$ACTIVE_COLOR" "$ACTIVE_VERSION"
|
|
142
645
|
cp "$ACTIVE_STATE" "$PREVIOUS_STATE.tmp"
|
|
@@ -145,27 +648,35 @@ rollback_version() {
|
|
|
145
648
|
}
|
|
146
649
|
|
|
147
650
|
case "${1:-}" in
|
|
651
|
+
build)
|
|
652
|
+
[[ "$#" -eq 1 ]] || usage
|
|
653
|
+
build_release
|
|
654
|
+
;;
|
|
148
655
|
start)
|
|
149
|
-
|
|
150
|
-
|
|
656
|
+
[[ "$#" -eq 1 ]] || usage
|
|
657
|
+
start_release
|
|
151
658
|
;;
|
|
152
659
|
stop)
|
|
153
|
-
|
|
660
|
+
[[ "$#" -eq 1 ]] || usage
|
|
661
|
+
initialize_server_context
|
|
662
|
+
stop_current
|
|
154
663
|
;;
|
|
155
664
|
restart)
|
|
665
|
+
[[ "$#" -eq 1 ]] || usage
|
|
666
|
+
initialize_server_context
|
|
156
667
|
restart_current
|
|
157
668
|
;;
|
|
158
669
|
status)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
670
|
+
[[ "$#" -eq 1 ]] || usage
|
|
671
|
+
initialize_server_context
|
|
672
|
+
status_current
|
|
162
673
|
;;
|
|
163
674
|
rollback)
|
|
164
|
-
|
|
165
|
-
|
|
675
|
+
[[ "$#" -eq 2 ]] || usage
|
|
676
|
+
initialize_server_context
|
|
677
|
+
rollback_version "$2"
|
|
166
678
|
;;
|
|
167
679
|
*)
|
|
168
|
-
|
|
169
|
-
exit 2
|
|
680
|
+
usage
|
|
170
681
|
;;
|
|
171
682
|
esac
|