# pear-git CI — auto-build + publish on `git push pear master`.
#
# Canonical bare-app distribution (path A): install the dependency closure, vendor the file: siblings
# (git-archive ships ONLY favela-web, so npm leaves broken symlinks), then `pgh publish .` content-
# addresses the app into the ReleaseRegistry → users `pgh app install favela-web`. Same pattern as
# atproto-bridge / hypernote.
steps:
- name: install deps
run: npm install --production
timeout: 300
# file:../ siblings — the CI git-archives ONLY favela-web into /tmp, so npm leaves broken symlinks and
# the published tree lacks these. They're self-contained (own node_modules), so vendor dereferenced
# copies (cp -rL follows the nested symlinks too — e.g. favela-community's hyper-rooms).
- name: verify vendored siblings are in hosted git history
run: |
# A vendored sibling is copied from the WORKING TREE, so whatever is checked out on this machine
# ships to the fleet — reviewed or not. Checked by hand on 2026-08-16 and it was already true:
# hyper-identity carried an uncommitted version bump and hyper-proto-peer an uncommitted publish
# lock, both of which would have shipped silently. atproto-bridge learned this the harder way
# (its ci.yaml records atproto-room shipping 27 unpushed commits on 2026-07-28).
#
# Makes git AUTHORITATIVE for what ships: refuse unless every sibling's HEAD is reachable from
# some hosted branch. Reachability, NOT equality with the default branch — atproto-room
# legitimately ships from statebase-fork-heal. Untracked files WARN rather than block (cp -rL
# ships them too, but they are usually probe scripts). Escape hatch: PGV3_ALLOW_UNPUSHED_SIBLINGS=1.
KB=/storage/projects/code/hypercore-kb/hyper-fleeky
[ -d "$KB" ] || KB="$HOME/hyper-fleeky"
SIBS=$(node -e 'const d=require("./package.json");const a={...d.dependencies,...d.devDependencies};console.log(Object.entries(a).filter(([,v])=>String(v).startsWith("file:")).map(([k])=>k).join(" "))')
[ -n "$SIBS" ] || { echo "no file: siblings found — package.json unreadable?"; exit 1; }
rc=0
for pkg in $SIBS; do
SRC="$KB/$pkg"; [ -d "$SRC/.git" ] || SRC="$KB/packages/$pkg"
[ -d "$SRC/.git" ] || { echo " $pkg: not a git checkout — skipped"; continue; }
GD="$HOME/git-repos/$pkg.git"
[ -d "$GD" ] || { echo " $pkg: NOT HOSTED ($GD) — refuse"; rc=1; continue; }
LOCAL=$(git -C "$SRC" rev-parse HEAD 2>/dev/null); SHORT=$(echo "$LOCAL" | cut -c1-7)
if [ -n "$(git -C "$SRC" status --porcelain --untracked-files=no 2>/dev/null | head -1)" ]; then
echo " $pkg: MODIFIED tracked files — refuse (commit or stash)"; rc=1; continue
fi
REFS=$(git --git-dir="$GD" for-each-ref --contains "$LOCAL" --format='%(refname:short)' refs/heads 2>/dev/null | tr '\n' ' ')
if [ -z "$REFS" ]; then echo " $pkg: $SHORT NOT in hosted history — refuse (push it)"; rc=1; continue; fi
U=$(git -C "$SRC" ls-files --others --exclude-standard 2>/dev/null | wc -l)
if [ "$U" -gt 0 ]; then echo " $pkg: $SHORT ok (on: $REFS) — WARN: $U untracked file(s) also vendored"
else echo " $pkg: $SHORT ok (on: $REFS)"; fi
done
if [ "$rc" -ne 0 ]; then
if [ -n "$PGV3_ALLOW_UNPUSHED_SIBLINGS" ]; then echo "PGV3_ALLOW_UNPUSHED_SIBLINGS set — continuing"; rc=0
else echo "REFUSING TO BUILD: a vendored sibling is not in hosted git history."; fi
fi
exit $rc
timeout: 60
- name: vendor sibling packages into the closure
run: |
# The list is DERIVED from package.json's file: deps — never hardcoded. It used to be spelled
# out here, and package.json then gained atproto-room and hyper-proto-peer without this line
# being touched, so every build published a tree with two BROKEN SYMLINKS. `pgh publish` caught
# it ("refusing to publish a tree that would crash at import") — but only at the last step, and
# only because that guard exists. Same drift the atproto-bridge ci.yaml documents; same fix.
KB=/storage/projects/code/hypercore-kb/hyper-fleeky
[ -d "$KB" ] || KB="$HOME/hyper-fleeky"
SIBS=$(node -e 'const d=require("./package.json");const a={...d.dependencies,...d.devDependencies};console.log(Object.entries(a).filter(([,v])=>String(v).startsWith("file:")).map(([k])=>k).join(" "))')
[ -n "$SIBS" ] || { echo "no file: siblings found — package.json unreadable?"; exit 1; }
echo "siblings (derived from package.json): $SIBS"
rc=0
for pkg in $SIBS; do
# hyper-identity lives under packages/, the rest at the top level — resolve both.
SRC="$KB/$pkg"; [ -d "$SRC" ] || SRC="$KB/packages/$pkg"
[ -d "$SRC" ] || { echo " $pkg: NO SOURCE at $KB/$pkg or $KB/packages/$pkg — refuse"; rc=1; continue; }
rm -rf "node_modules/$pkg" && cp -rL "$SRC" "node_modules/$pkg" && echo " vendored $pkg <- $SRC"
done
exit $rc
timeout: 240
- name: publish to the release registry
# --accept-conflict: favela-web vendors the atproto-* siblings at their CURRENT versions (e.g.
# atproto-identity 0.0.2 with the lens-export genesis entries), which differ from other apps'
# already-published copies in the shared content-addressed store. Expected cross-app dep drift.
run: pgh publish . --accept-conflict "favela-web vendors atproto-* siblings at current versions (lens-export); differs from other apps' published copies"
timeout: 300