first commit
This commit is contained in:
Executable
+158
@@ -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}"
|
||||
Executable
+42
@@ -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)
|
||||
}
|
||||
Executable
+33
@@ -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'}`)
|
||||
Executable
+34
@@ -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/"
|
||||
Executable
+64
@@ -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"
|
||||
Executable
+53
@@ -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)
|
||||
Reference in New Issue
Block a user