Switch Lotus Cinny from nightly dev to stable-release fork workflow
Lint / Shell (shellcheck) (push) Successful in 9s
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (ruff) (push) Successful in 5s
Lint / Python deps (pip-audit) (push) Successful in 55s
Lint / Secret scan (gitleaks) (push) Successful in 5s

- Replace nightly build script with daily upstream release checker
  (cinny/upstream-check.sh) — posts to Matrix as LotusBot when a new
  cinnyapp/cinny stable release is published
- Add cinny/lotus-build.sh — merges latest release tag into the lotus
  branch, builds, deploys; triggered via !cinny-update webhook
- Fork lives at code.lotusguild.org/LotusGuild/cinny (lotus branch, v4.11.1)
- deploy/hooks-lxc106.json — adds cinny-build webhook endpoint (port 9000)
- Update landing page: "dev branch / nightly" → "Lotus fork / stable releases"
- Set LotusBot avatar on @hookshot_tinker-tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 16:57:35 -04:00
parent 278f850f0c
commit 7f7ddd3e3c
9 changed files with 340 additions and 95 deletions
-59
View File
@@ -1,59 +0,0 @@
#!/bin/bash
# Cinny dev branch nightly build and deploy
set -e
REPO_DIR="/opt/cinny-dev"
WEB_ROOT="/var/www/html"
CONFIG_SAVED="/opt/cinny-dev/.cinny-config.json"
BUILD_DIR="/opt/cinny-dev/dist"
LOG="/var/log/cinny-dev-update.log"
exec >> "$LOG" 2>&1
echo "=== $(date) === Starting Cinny dev update ==="
# Save config from live site (skip if web root is already broken)
if [ -f "$WEB_ROOT/config.json" ]; then
cp "$WEB_ROOT/config.json" "$CONFIG_SAVED"
fi
# Pull latest dev
cd "$REPO_DIR"
git fetch --depth=1 origin dev
PREV=$(git rev-parse HEAD)
git reset --hard origin/dev
NEW=$(git rev-parse HEAD)
if [ "$PREV" = "$NEW" ]; then
echo "No changes since last build ($NEW), skipping."
exit 0
fi
echo "New commits since $PREV, building $NEW..."
# Clean previous dist so stale files don't get deployed on partial build
rm -rf "$BUILD_DIR"
# Build — cap at 896MB to stay within 1GB RAM + swap headroom
export NODE_OPTIONS='--max_old_space_size=896'
npm ci 2>&1 | tail -5
npm run build 2>&1 | tail -10
# Verify build actually produced output before touching web root
if [ ! -f "$BUILD_DIR/index.html" ]; then
echo "ERROR: Build failed — dist/index.html missing. Web root untouched."
exit 1
fi
# Deploy only now that we know the build succeeded
rm -rf "${WEB_ROOT:?}"/*
cp -r "$BUILD_DIR"/* "$WEB_ROOT/"
# Restore config
if [ -f "$CONFIG_SAVED" ]; then
cp "$CONFIG_SAVED" "$WEB_ROOT/config.json"
else
echo "WARNING: No saved config found — default config from build will be used."
fi
nginx -s reload
echo "=== Deploy complete: $NEW ==="
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# Merges the latest upstream stable release tag into the lotus branch, builds, and deploys.
# Triggered via webhook by LotusBot !cinny-update command.
# Requires:
# /etc/cinny-monitor.env — MATRIX_TOKEN, MATRIX_SERVER, MATRIX_ROOM
# /opt/lotus-cinny/ — git clone of code.lotusguild.org/LotusGuild/cinny
# with upstream remote: https://github.com/cinnyapp/cinny.git
set -euo pipefail
REPO_DIR="/opt/lotus-cinny"
WEB_ROOT="/var/www/html"
CONFIG_BACKUP="/opt/lotus-cinny/.cinny-config.json"
BUILD_DIR="/opt/lotus-cinny/dist"
STATE_FILE="/var/lib/cinny-monitor/last-upstream-tag"
ENV_FILE="/etc/cinny-monitor.env"
LOG="/var/log/cinny-build.log"
exec >> "$LOG" 2>&1
echo "=== $(date) === Cinny build triggered ==="
[ -f "$ENV_FILE" ] || { echo "ERROR: $ENV_FILE missing"; exit 1; }
set -a
# shellcheck source=/dev/null
source "$ENV_FILE"
set +a
matrix_notify() {
local msg="$1"
echo "[notify] $msg"
[ -z "${MATRIX_TOKEN:-}" ] && return
export _NOTIFY_MSG="$msg"
python3 << 'PYEOF'
import urllib.request, urllib.parse, json, time, sys, os
token = os.environ.get('MATRIX_TOKEN', '')
server = os.environ.get('MATRIX_SERVER', '').rstrip('/')
room = os.environ.get('MATRIX_ROOM', '')
msg = os.environ.get('_NOTIFY_MSG', '')
if not all([token, server, room, msg]):
sys.exit(0)
txn = str(int(time.time() * 1000))
api = f"{server}/_matrix/client/v3/rooms/{urllib.parse.quote(room, safe='')}/send/m.room.message/{txn}"
body = json.dumps({"msgtype": "m.text", "body": msg}).encode()
req = urllib.request.Request(api, data=body, method="PUT", headers={
"Authorization": f"Bearer {token}", "Content-Type": "application/json"
})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"Notify failed: {e}", file=sys.stderr)
PYEOF
}
if [ ! -d "$REPO_DIR/.git" ]; then
matrix_notify "cinny-build: FAILED — $REPO_DIR is not a git repo. Clone the lotus fork first."
exit 1
fi
cd "$REPO_DIR"
if ! git remote | grep -q '^upstream$'; then
matrix_notify "cinny-build: FAILED — upstream remote missing. Run: git remote add upstream https://github.com/cinnyapp/cinny.git"
exit 1
fi
matrix_notify "cinny-build: fetching upstream tags..."
git fetch upstream --tags --no-recurse-submodules -q
# Get latest stable release tag from GitHub API
LATEST_TAG=$(curl -sf \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: lotus-cinny-monitor/1.0" \
"https://api.github.com/repos/cinnyapp/cinny/releases/latest" | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
CURRENT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || git log -1 --format='%D' | grep -oP 'tag: \K[^,]+' | head -1 || echo "untagged")
echo "Current: $CURRENT_TAG — Target: $LATEST_TAG"
if [ "$CURRENT_TAG" = "$LATEST_TAG" ]; then
matrix_notify "cinny-build: already on $LATEST_TAG, nothing to do."
exit 0
fi
matrix_notify "cinny-build: merging $LATEST_TAG into lotus branch..."
# Back up live config before touching anything
[ -f "$WEB_ROOT/config.json" ] && cp "$WEB_ROOT/config.json" "$CONFIG_BACKUP"
if ! git merge "$LATEST_TAG" --no-edit 2>&1; then
git merge --abort 2>/dev/null || true
matrix_notify "cinny-build: FAILED — merge conflict at $LATEST_TAG. SSH to LXC 106 and resolve manually. See /var/log/cinny-build.log"
exit 1
fi
echo "Running npm ci..."
npm ci 2>&1 | tail -5
rm -rf "$BUILD_DIR"
export NODE_OPTIONS='--max_old_space_size=896'
echo "Building $LATEST_TAG..."
npm run build 2>&1 | tail -10
if [ ! -f "$BUILD_DIR/index.html" ]; then
matrix_notify "cinny-build: FAILED — build produced no output at $LATEST_TAG. See /var/log/cinny-build.log"
exit 1
fi
rm -rf "${WEB_ROOT:?}"/*
cp -r "$BUILD_DIR"/* "$WEB_ROOT/"
[ -f "$CONFIG_BACKUP" ] && cp "$CONFIG_BACKUP" "$WEB_ROOT/config.json"
nginx -s reload
# Push merged lotus branch to origin
git push origin lotus
# Update state file so upstream-check knows we're on this tag
echo "$LATEST_TAG" > "$STATE_FILE"
matrix_notify "cinny-build: deployed $LATEST_TAG — Lotus Cinny is live."
echo "=== Build complete: $LATEST_TAG ==="
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Checks if cinnyapp/cinny has published a new stable release; notifies Matrix if so.
# Runs daily at noon via /etc/cron.d/cinny-upstream-check
# Requires /etc/cinny-monitor.env:
# MATRIX_TOKEN, MATRIX_SERVER, MATRIX_ROOM, MATRIX_PING_USER
set -euo pipefail
ENV_FILE="/etc/cinny-monitor.env"
STATE_FILE="/var/lib/cinny-monitor/last-upstream-tag"
LOG="/var/log/cinny-monitor.log"
exec >> "$LOG" 2>&1
echo "=== $(date) === Cinny upstream release check ==="
[ -f "$ENV_FILE" ] || { echo "ERROR: $ENV_FILE missing"; exit 1; }
set -a
# shellcheck source=/dev/null
source "$ENV_FILE"
set +a
mkdir -p "$(dirname "$STATE_FILE")"
API_RESPONSE=$(curl -sf \
-H "Accept: application/vnd.github.v3+json" \
-H "User-Agent: lotus-cinny-monitor/1.0" \
"https://api.github.com/repos/cinnyapp/cinny/releases/latest") || {
echo "ERROR: GitHub API request failed"
exit 1
}
LATEST_TAG=$(echo "$API_RESPONSE" | jq -r '.tag_name')
LATEST_NAME=$(echo "$API_RESPONSE" | jq -r '.name')
RELEASE_URL=$(echo "$API_RESPONSE" | jq -r '.html_url')
RELEASE_BODY=$(echo "$API_RESPONSE" | jq -r '.body | split("\n")[0:3] | join(" | ")')
LAST_TAG=""
[ -f "$STATE_FILE" ] && LAST_TAG=$(cat "$STATE_FILE")
if [ "$LATEST_TAG" = "$LAST_TAG" ]; then
echo "No new release (still at $LATEST_TAG), nothing to do."
exit 0
fi
echo "New release: $LATEST_TAG (was: ${LAST_TAG:-none})"
export _CM_LATEST_TAG="$LATEST_TAG"
export _CM_LATEST_NAME="$LATEST_NAME"
export _CM_RELEASE_URL="$RELEASE_URL"
export _CM_RELEASE_BODY="$RELEASE_BODY"
export _CM_LAST_TAG="$LAST_TAG"
python3 << 'PYEOF'
import urllib.request, urllib.parse, json, time, sys, os
token = os.environ.get('MATRIX_TOKEN', '')
server = os.environ.get('MATRIX_SERVER', '').rstrip('/')
room = os.environ.get('MATRIX_ROOM', '')
ping = os.environ.get('MATRIX_PING_USER', '')
if not all([token, server, room]):
print("ERROR: MATRIX_TOKEN/MATRIX_SERVER/MATRIX_ROOM not set in env file")
sys.exit(1)
tag = os.environ['_CM_LATEST_TAG']
name = os.environ['_CM_LATEST_NAME']
url = os.environ['_CM_RELEASE_URL']
body = os.environ['_CM_RELEASE_BODY']
last_tag = os.environ['_CM_LAST_TAG']
from_str = f" (was {last_tag})" if last_tag else ""
ping_safe = urllib.parse.quote(ping, safe=":@")
plain = (
f"{ping}: Cinny {tag} released{from_str}.\n"
f"{body}\n"
f"Review release notes, then send !cinny-update to merge + rebuild.\n{url}"
)
html = (
f"<a href='https://matrix.to/#/{ping_safe}'>{ping}</a>: "
f"<strong>Cinny {tag} released</strong>{from_str}.<br>"
f"<em>{body}</em><br>"
f"<a href='{url}'>Release notes</a> · Send <code>!cinny-update</code> to merge + rebuild."
)
txn = str(int(time.time() * 1000))
api = f"{server}/_matrix/client/v3/rooms/{urllib.parse.quote(room, safe='')}/send/m.room.message/{txn}"
body_json = json.dumps({"msgtype": "m.text", "body": plain,
"format": "org.matrix.custom.html", "formatted_body": html}).encode()
req = urllib.request.Request(api, data=body_json, method="PUT", headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
})
urllib.request.urlopen(req, timeout=10)
print(f"Notification sent for {tag}")
PYEOF
echo "$LATEST_TAG" > "$STATE_FILE"
echo "State updated to $LATEST_TAG"