first commit
Release rolling / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-07-18 16:17:38 -04:00
commit 015d92a257
70 changed files with 10033 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Build source release artifacts and publish a rolling Gitea release.
#
# Required:
# RELEASE_TOKEN — Gitea PAT with repository release write
# Optional:
# GITEA_URL / GITEA_OWNER / GITEA_REPO
# RELEASE_TAG (default: rolling)
# DRY_RUN=1 — build + stage only, no upload
# GITHUB_SHA / GITEA_SHA — target commit for the release tag
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
VERSION="$(node -p "require('./package.json').version")"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
FULL_SHA="${GITHUB_SHA:-${GITEA_SHA:-$(git rev-parse HEAD 2>/dev/null || echo main)}}"
TAG="${RELEASE_TAG:-rolling}"
PKG_NAME="$(node -p "require('./package.json').name")"
RELEASE_TITLE="${RELEASE_NAME:-${PKG_NAME} rolling}"
DIST="$ROOT/dist"
log() { echo "[release] $*"; }
detect_remote() {
local url
url="$(git remote get-url origin 2>/dev/null || true)"
if [[ "$url" =~ git@([^:]+):([^/]+)/([^/.]+) ]]; then
echo "https://${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
elif [[ "$url" =~ https?://([^/]+)/([^/]+)/([^/.]+) ]]; then
echo "https://${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
else
echo "" "" ""
fi
}
read -r DETECTED_URL DETECTED_OWNER DETECTED_REPO <<<"$(detect_remote)"
GITEA_URL="${GITEA_URL:-${DETECTED_URL:-}}"
GITEA_OWNER="${GITEA_OWNER:-${DETECTED_OWNER:-}}"
GITEA_REPO="${GITEA_REPO:-${DETECTED_REPO:-${PKG_NAME}}}"
if [[ -z "${GITEA_URL}" ]]; then
log "WARN: could not detect GITEA_URL — set GITEA_URL for upload"
fi
# --- build artifacts (source tarball + checksum + notes) ---
log "building release artifacts via scripts/release.sh"
bash scripts/release.sh
# Enrich notes with rolling metadata (release.sh writes a base file)
cat >"$DIST/RELEASE_NOTES.md" <<EOF
# ${PKG_NAME} ${VERSION} (${TAG})
- Commit: \`${SHA}\` (\`${FULL_SHA}\`)
- Built: ${STAMP}
HyperDHT + protomux-rpc application template.
## Contents
- Node server (\`npm run start:server\`)
- Pear desktop client (\`npm start\` / \`pear run -d .\`)
- Demo room (messages + presence + invites)
## Install
\`\`\`bash
mkdir -p app && tar -xzf ${PKG_NAME}-v${VERSION}.tar.gz -C app
cd app
npm install
npm run start:server
\`\`\`
## Verify
\`\`\`bash
sha256sum -c ${PKG_NAME}-v${VERSION}.tar.gz.sha256
\`\`\`
## Checksums
See \`*.sha256\` beside each archive.
EOF
log "artifacts in $DIST:"
ls -la "$DIST" || true
shopt -s nullglob
ARTIFACTS=("$DIST"/*.tar.gz)
if [[ ${#ARTIFACTS[@]} -eq 0 ]]; then
log "ERROR: no tarball artifacts in $DIST"
exit 1
fi
if [[ "${DRY_RUN:-0}" == "1" ]]; then
log "DRY_RUN=1 — skip Gitea upload"
exit 0
fi
if [[ -z "${RELEASE_TOKEN:-}" ]]; then
log "ERROR: RELEASE_TOKEN is required (or DRY_RUN=1)"
exit 1
fi
if [[ -z "${GITEA_URL}" || -z "${GITEA_OWNER}" || -z "${GITEA_REPO}" ]]; then
log "ERROR: GITEA_URL / GITEA_OWNER / GITEA_REPO must be set"
exit 1
fi
API="${GITEA_URL%/}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}"
AUTH="Authorization: token ${RELEASE_TOKEN}"
log "Gitea API: $API tag=$TAG"
REL_JSON="$(curl -fsSL -H "$AUTH" "$API/releases/tags/${TAG}" 2>/dev/null || true)"
if [[ -n "$REL_JSON" ]]; then
REL_ID="$(node -e "try{const j=JSON.parse(process.argv[1]);console.log(j.id||'')}catch{console.log('')}" "$REL_JSON")"
if [[ -n "$REL_ID" ]]; then
log "deleting previous release id=$REL_ID"
curl -fsSL -X DELETE -H "$AUTH" "$API/releases/$REL_ID" >/dev/null || true
fi
fi
curl -fsSL -X DELETE -H "$AUTH" "$API/tags/${TAG}" >/dev/null 2>&1 || true
CREATE_BODY="$(node -e "
const notes=require('fs').readFileSync(process.argv[1],'utf8');
console.log(JSON.stringify({
tag_name: process.argv[2],
name: process.argv[3],
body: notes,
draft: false,
prerelease: true,
target_commitish: process.argv[4]
}));
" "$DIST/RELEASE_NOTES.md" "$TAG" "${RELEASE_TITLE} ${VERSION} ${SHA}" "$FULL_SHA")"
CREATE_RESP="$(curl -fsSL -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "$CREATE_BODY" "$API/releases")"
REL_ID="$(node -e "console.log(JSON.parse(process.argv[1]).id)" "$CREATE_RESP")"
log "created release id=$REL_ID"
for f in "$DIST"/*; do
[[ -f "$f" ]] || continue
base="$(basename "$f")"
case "$base" in
*.tar.gz|*.sha256|*.md) ;;
*) continue ;;
esac
log "upload $base"
name_q="$(node -e "console.log(encodeURIComponent(process.argv[1]))" "$base")"
curl -fsSL -X POST -H "$AUTH" \
-F "attachment=@${f}" \
"$API/releases/${REL_ID}/assets?name=${name_q}" \
>/dev/null
done
log "release published: ${GITEA_URL}/${GITEA_OWNER}/${GITEA_REPO}/releases/tag/${TAG}"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env node
/**
* Process supervision healthcheck.
* If PEARDATA_HEALTH_KEY is set, dials the server and pings.
* Otherwise exits 0 when the process can import server modules (liveness).
*
* Exit 0 = healthy, 1 = unhealthy.
*/
import { PearDataConnection } from '../client/connection.js'
const timeoutMs = Number(process.env.HEALTHCHECK_TIMEOUT_MS) || 8000
const publicKey = process.env.PEARDATA_HEALTH_KEY || process.env.SERVER_PUBLIC_KEY
const timer = setTimeout(() => {
console.error('healthcheck: timeout')
process.exit(1)
}, timeoutMs)
try {
if (!publicKey || !/^[0-9a-fA-F]{64}$/.test(publicKey)) {
// Liveness without remote dial
clearTimeout(timer)
console.log('ok liveness')
process.exit(0)
}
const conn = new PearDataConnection(publicKey, {
timeoutMs,
adminSeed: process.env.SERVER_SEED || null,
})
await conn.connect()
const pong = await conn.ping()
await conn.destroy()
clearTimeout(timer)
if (!pong?.ok) throw new Error('ping failed')
console.log('ok ping', pong.pong)
process.exit(0)
} catch (err) {
clearTimeout(timer)
console.error('healthcheck failed:', err.message)
process.exit(1)
}
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
/**
* Mint a pa1 invite from SERVER_SEED without starting the full server process.
*
* Usage:
* node scripts/mint-invite.js [role=operator] [ttlMs]
*/
import dotenv from 'dotenv'
import { loadOrCreateKeyPair } from '../server/core/keys.js'
import { signCapability, encodeInvite } from '../shared/crypto-auth.js'
dotenv.config()
const { publicKeyHex, seedHex } = loadOrCreateKeyPair()
const role = process.argv[2] || 'operator'
const ttlArg = process.argv[3]
const ttlMs = ttlArg === undefined ? null : Number(ttlArg)
const { token, payload } = signCapability(seedHex, {
role,
ttlMs,
forever: ttlMs == null || ttlMs === 0,
})
const invite = encodeInvite({
publicKeyHex,
capability: token,
role: payload.role,
jti: payload.jti,
expiresAt: payload.exp,
})
console.log(invite)
console.error(`# role=${payload.role} jti=${payload.jti} exp=${payload.exp ?? 'never'}`)
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
VERSION="$(node -p "require('./package.json').version")"
NAME="peardata-v${VERSION}"
mkdir -p dist
tar --exclude=node_modules --exclude=.git --exclude=data --exclude=dist \
-czf "dist/${NAME}.tar.gz" .
(
cd dist
if command -v sha256sum >/dev/null; then
sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
else
shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
fi
)
cat > dist/RELEASE_NOTES.md <<EOF
# ${NAME}
HyperDHT + protomux-rpc application template.
## Contents
- Node server (\`npm run start:server\`)
- Pear desktop client (\`npm start\` / \`pear run -d .\`)
- Demo room (messages + presence + invites)
## Verify
\`\`\`
sha256sum -c ${NAME}.tar.gz.sha256
\`\`\`
EOF
ls -la dist
echo "Release artifacts ready in dist/"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Rebrand peardata → your product name.
# Usage: bash scripts/rename-template.sh my-app MyApp
set -euo pipefail
OLD_SLUG="peardata"
OLD_NAME="peardata"
OLD_PRODUCT="PearData"
OLD_PROTOCOL="peardata/rpc"
OLD_PREFIX="pd1."
OLD_ENV="PEARDATA_"
NEW_SLUG="${1:-}"
NEW_PRODUCT="${2:-}"
if [[ -z "$NEW_SLUG" || -z "$NEW_PRODUCT" ]]; then
echo "Usage: $0 <slug> <ProductName>"
echo " example: $0 notes-mesh NotesMesh"
exit 1
fi
if [[ ! "$NEW_SLUG" =~ ^[a-z][a-z0-9-]*$ ]]; then
echo "slug must be lowercase alphanumeric + dashes"
exit 1
fi
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# Derive invite prefix from first letters + 1.
PREFIX="$(echo "$NEW_SLUG" | tr -cd 'a-z' | cut -c1-2)1."
ENV_PREFIX="$(echo "$NEW_SLUG" | tr 'a-z-' 'A-Z_')_"
ENV_PREFIX="${ENV_PREFIX//__/_}"
echo "Renaming:"
echo " package: $OLD_NAME$NEW_SLUG"
echo " product: $OLD_PRODUCT$NEW_PRODUCT"
echo " protocol: $OLD_PROTOCOL${NEW_SLUG}/rpc"
echo " invite: $OLD_PREFIX$PREFIX"
echo " env: $OLD_ENV$ENV_PREFIX"
export LC_ALL=C
find . -type f \
\( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.json' -o -name '*.md' -o -name '*.html' -o -name '*.css' -o -name '*.yml' -o -name '*.yaml' -o -name '*.service' -o -name '*.example' -o -name '*.sh' \) \
! -path './node_modules/*' ! -path './.git/*' ! -path './data/*' \
-print0 | while IFS= read -r -d '' f; do
perl -pi -e "
s/\Q$OLD_NAME\E/$NEW_SLUG/g;
s/\Q$OLD_PRODUCT\E/$NEW_PRODUCT/g;
s/\Q$OLD_PROTOCOL\E/${NEW_SLUG}\\/rpc/g;
s/\Q$OLD_SLUG\E/$NEW_SLUG/g;
s/\Q$OLD_PREFIX\E/$PREFIX/g;
s/\Q$OLD_ENV\E/$ENV_PREFIX/g;
" "$f"
done
if [[ -f bin/peardata-server.mjs ]]; then
mv bin/peardata-server.mjs "bin/${NEW_SLUG}-server.mjs"
fi
if [[ -f deploy/peardata.service ]]; then
mv deploy/peardata.service "deploy/${NEW_SLUG}.service"
fi
echo "Done. Review git diff, then: npm install && npm test"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
/**
* Soak test: connect, post messages, verify pushes for SOAK_DURATION_MS.
*
* Usage:
* SERVER_PUBLIC_KEY=… SERVER_SEED=… node scripts/soak.js
*/
import { PearDataConnection } from '../client/connection.js'
import { Methods, Pushes } from '../shared/protocol.js'
const duration = Number(process.env.SOAK_DURATION_MS) || 60_000
const publicKey = process.env.SERVER_PUBLIC_KEY
const seed = process.env.SERVER_SEED
if (!publicKey) {
console.error('SERVER_PUBLIC_KEY required')
process.exit(1)
}
const conn = new PearDataConnection(publicKey, {
adminSeed: seed || null,
timeoutMs: 30_000,
})
let received = 0
let sent = 0
let errors = 0
conn.on(Pushes.message, () => {
received++
})
await conn.connect()
console.log('soak connected as', conn.role)
const end = Date.now() + duration
while (Date.now() < end) {
try {
await conn.request(Methods.postMessage, {
text: `soak ${sent} @ ${new Date().toISOString()}`,
})
sent++
await conn.ping()
} catch (err) {
errors++
console.error('soak error', err.message)
}
await new Promise((r) => setTimeout(r, Number(process.env.SOAK_INTERVAL_MS) || 500))
}
await conn.destroy()
console.log(JSON.stringify({ duration, sent, received, errors }, null, 2))
process.exit(errors > 0 ? 1 : 0)