feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
DEFAULT_SSH_HOST,
|
||||
NGINX_MARKER,
|
||||
NGINX_SITE_LINK,
|
||||
NGINX_SNIPPET,
|
||||
SERVER_PUBLIC_ROOT,
|
||||
SERVER_RELEASES_ROOT,
|
||||
SERVER_STAGING_ROOT,
|
||||
parseOptions,
|
||||
renderNoReleasePage,
|
||||
runSsh,
|
||||
usageError,
|
||||
validateSshHost
|
||||
} from './lib.mjs'
|
||||
|
||||
const usage = `Usage:
|
||||
node scripts/updates/provision-server.mjs [--ssh-host local001]
|
||||
|
||||
Creates /srv/ycode, /srv/ycode/releases/versions and /srv/ycode-staging, backs up the
|
||||
nginx site configuration, installs a YCode-only snippet, validates nginx, then reloads.
|
||||
It never serves an index and never creates a fake latest.json.
|
||||
`
|
||||
|
||||
const snippet = `${NGINX_MARKER}
|
||||
location = /ycode/releases/latest.json {
|
||||
alias ${SERVER_RELEASES_ROOT}/latest.json;
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
limit_except GET { deny all; }
|
||||
}
|
||||
|
||||
location = /ycode/releases/index.html {
|
||||
alias ${SERVER_RELEASES_ROOT}/index.html;
|
||||
default_type text/html;
|
||||
charset utf-8;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
limit_except GET { deny all; }
|
||||
}
|
||||
|
||||
location /ycode/ {
|
||||
alias ${SERVER_PUBLIC_ROOT}/;
|
||||
autoindex off;
|
||||
index index.html;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
limit_except GET { deny all; }
|
||||
}
|
||||
`
|
||||
|
||||
async function main() {
|
||||
const options = parseOptions(process.argv.slice(2), { values: ['ssh-host'] })
|
||||
const host = validateSshHost(options['ssh-host'] || DEFAULT_SSH_HOST)
|
||||
const snippetBase64 = Buffer.from(snippet, 'utf8').toString('base64')
|
||||
const noReleasePageBase64 = Buffer.from(renderNoReleasePage(), 'utf8').toString('base64')
|
||||
const script = `set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
site_link='${NGINX_SITE_LINK}'
|
||||
snippet='${NGINX_SNIPPET}'
|
||||
site="$(readlink -f "$site_link")"
|
||||
test -f "$site"
|
||||
test -f /etc/nginx/nginx.conf
|
||||
nginx -t >/dev/null 2>&1
|
||||
|
||||
install -d -o root -g www-data -m 0755 '${SERVER_PUBLIC_ROOT}'
|
||||
install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}'
|
||||
install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}/versions'
|
||||
install -d -o root -g root -m 0700 '${SERVER_STAGING_ROOT}'
|
||||
|
||||
page_tmp="$(mktemp '${SERVER_RELEASES_ROOT}/.index.html.XXXXXX')"
|
||||
printf '%s' '${noReleasePageBase64}' | base64 -d > "$page_tmp"
|
||||
chmod 0644 "$page_tmp"
|
||||
if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then
|
||||
if [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then
|
||||
rm -f "$page_tmp"
|
||||
echo "latest.json exists without a generated download page" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then
|
||||
rm -f "$page_tmp"
|
||||
echo "latest.json exists while the download page still says no release" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$page_tmp"
|
||||
elif [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then
|
||||
mv -f "$page_tmp" '${SERVER_RELEASES_ROOT}/index.html'
|
||||
else
|
||||
if ! grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then
|
||||
rm -f "$page_tmp"
|
||||
echo "download page exists without latest.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$page_tmp"
|
||||
fi
|
||||
|
||||
backup_dir=/var/backups/nginx
|
||||
install -d -o root -g root -m 0700 "$backup_dir"
|
||||
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
config_backup="$backup_dir/ycode-before-${'$'}{stamp}-$(basename "$site")"
|
||||
snippet_backup="$backup_dir/ycode-before-${'$'}{stamp}-ycode-releases.conf"
|
||||
test ! -e "$config_backup"
|
||||
cp -a "$site" "$config_backup"
|
||||
|
||||
snippet_existed=0
|
||||
if [ -f "$snippet" ]; then
|
||||
cp -a "$snippet" "$snippet_backup"
|
||||
snippet_existed=1
|
||||
fi
|
||||
|
||||
restore() {
|
||||
changed=0
|
||||
cp -a "$config_backup" "$site"
|
||||
if [ "$snippet_existed" -eq 1 ]; then
|
||||
cp -a "$snippet_backup" "$snippet"
|
||||
else
|
||||
rm -f "$snippet"
|
||||
fi
|
||||
}
|
||||
|
||||
changed=0
|
||||
snippet_tmp=
|
||||
cleanup() {
|
||||
rc=$?
|
||||
if [ -n "$snippet_tmp" ]; then
|
||||
rm -f "$snippet_tmp"
|
||||
fi
|
||||
if [ "$rc" -ne 0 ] && [ "$changed" -eq 1 ]; then
|
||||
restore
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit "$rc"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
install -d -o root -g root -m 0755 /etc/nginx/snippets
|
||||
snippet_tmp="$(mktemp /etc/nginx/snippets/.ycode-releases.XXXXXX)"
|
||||
printf '%s' '${snippetBase64}' | base64 -d > "$snippet_tmp"
|
||||
chmod 0644 "$snippet_tmp"
|
||||
mv -f "$snippet_tmp" "$snippet"
|
||||
changed=1
|
||||
|
||||
python3 - "$site" <<'PY'
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
site = sys.argv[1]
|
||||
marker = "${NGINX_MARKER}"
|
||||
include = "\tinclude ${NGINX_SNIPPET};"
|
||||
with open(site, "r", encoding="utf-8") as stream:
|
||||
original = stream.read()
|
||||
|
||||
for required in ("location /baocode/ {", "location /baocode/releases/ {"):
|
||||
if original.count(required) != 1:
|
||||
raise SystemExit(f"refusing to change nginx: expected one {required}")
|
||||
|
||||
if marker in original:
|
||||
if include not in original:
|
||||
raise SystemExit("refusing to change nginx: YCode marker exists without include")
|
||||
updated = original
|
||||
else:
|
||||
match = re.search(r"(?m)^([ \\t]*)location /baocode/ \\{", original)
|
||||
if not match:
|
||||
raise SystemExit("refusing to change nginx: baocode anchor not found")
|
||||
indent = match.group(1)
|
||||
insertion = f"{indent}{marker}\\n{indent}include ${NGINX_SNIPPET};\\n\\n"
|
||||
updated = original[:match.start()] + insertion + original[match.start():]
|
||||
|
||||
if updated != original:
|
||||
directory = os.path.dirname(site)
|
||||
descriptor, temporary = tempfile.mkstemp(prefix=".ycode-nginx-", dir=directory)
|
||||
try:
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
||||
stream.write(updated)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
shutil.copystat(site, temporary)
|
||||
os.replace(temporary, site)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
with open(site, "r", encoding="utf-8") as stream:
|
||||
final = stream.read()
|
||||
for required in ("location /baocode/ {", "location /baocode/releases/ {"):
|
||||
if final.count(required) != 1:
|
||||
raise SystemExit("refusing to change nginx: baocode locations changed")
|
||||
if marker not in final or include not in final:
|
||||
raise SystemExit("nginx include was not installed")
|
||||
PY
|
||||
|
||||
if ! nginx -t; then
|
||||
echo "nginx validation failed; configuration restored" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! systemctl reload nginx; then
|
||||
echo "nginx reload failed; configuration restored" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/latest.json || true)"
|
||||
if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then
|
||||
expected_status=200
|
||||
else
|
||||
expected_status=404
|
||||
fi
|
||||
if [ "$status" != "$expected_status" ]; then
|
||||
echo "unexpected /ycode/releases/latest.json status: $status (expected $expected_status)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
page_status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/ || true)"
|
||||
if [ "$page_status" != "200" ]; then
|
||||
echo "unexpected /ycode/releases/ status: $page_status (expected 200)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'site=%s\\n' "$site"
|
||||
printf 'backup=%s\\n' "$config_backup"
|
||||
printf 'snippet=%s\\n' "$snippet"
|
||||
printf 'latest_status=%s\\n' "$status"
|
||||
printf 'page_status=%s\\n' "$page_status"
|
||||
`
|
||||
|
||||
const output = runSsh(host, script)
|
||||
process.stdout.write(output)
|
||||
console.log(`Provisioned ${host}; no release manifest was created.`)
|
||||
}
|
||||
|
||||
main().catch((error) => usageError(usage, error))
|
||||
Reference in new issue
Block a user