OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / .pear-git / ci.yaml

123 lines · yaml ref:
# pear-git CI — auto-build + publish on `git push pear master`.
#
# Canonical bare-app distribution (PATH A): install the root closure, then `pgh publish .`
# content-addresses the app into the ReleaseRegistry so it is installable via
# `pgh app install ap-bridge`. NOT the tarball/`artifacts:` path — that only serves a blob
# over hyper-deploy and is not installable. Same shape as atproto-bridge / hypernote.
#
# ap-bridge DOES have file:../ siblings (atproto-did-peer and its own siblings), so it needs
# the vendor step: CI git-archives ONLY this repo into /tmp, leaving each file: dep as a broken
# symlink — the published tree would lack them entirely and the daemon would crash-loop
# MODULE_NOT_FOUND. Same shape as atproto-bridge's vendor steps.
#
# NO server-boot smoke here: CI steps are synchronous with a hard timeout, and a step that
# boots a long-running server orphans the child when the timeout fires.

steps:
  - name: install deps (root closure)
    run: npm install --production
    timeout: 300

  # Vendor LAST — after every npm operation, immediately before the guard and publish. A vendored
  # sibling is copied from the WORKING TREE, so whatever is checked out on the build machine ships
  # to the fleet, reviewed or not. Refuse unless each sibling HEAD is reachable from some hosted
  # branch (reachability, not equality with a default branch: a sibling may legitimately ship from
  # a feature branch). This already caught atproto-did-peer and atproto-store carrying unpushed
  # commits that the outbox projection depends on.
  - name: "vendor file: siblings into the closure"
    run: |
      #
      # The list is DERIVED from package.json's file: deps — never hardcoded. atproto-bridge kept it
      # in three hardcoded places, one of them drifted when favela-community was added, and `pgh
      # publish` failed on every build for ~10 days without anyone noticing (a failed publish records
      # no version, so `pgh deploy log` simply showed nothing newer). Deriving it means a sibling
      # cannot be half-wired again.
      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(" "))')
      echo "siblings (derived from package.json): $SIBS"
      [ -n "$SIBS" ] || { echo "no file: siblings found — package.json unreadable?"; exit 1; }
      rc=0
      for pkg in $SIBS; do
        SRC=/storage/projects/code/hypercore-kb/hyper-fleeky/$pkg
        [ -d "$SRC" ] || SRC="$HOME/hyper-fleeky/$pkg"
        if [ ! -d "$SRC" ]; then echo "  $pkg: NOT FOUND on the build machine — refuse"; rc=1; continue; fi
        GD="$HOME/git-repos/$pkg.git"
        if [ -d "$SRC/.git" ] && [ -d "$GD" ]; then
          LOCAL=$(git -C "$SRC" rev-parse HEAD 2>/dev/null)
          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: $(echo $LOCAL | cut -c1-7) NOT in hosted history — refuse (push it)"; rc=1; continue; fi
          echo "  $pkg: $(echo $LOCAL | cut -c1-7) ok (on: $REFS)"
        fi
        rm -rf "node_modules/$pkg" && cp -rL "$SRC" "node_modules/$pkg"
      done
      exit $rc
    timeout: 180

  # The declare-your-deps guard. An undeclared direct import resolves fine locally via npm
  # hoisting and then crash-loops the --production closure with MODULE_NOT_FOUND — the class of
  # bug behind two prior atproto-bridge outages. b4a and bare-fs were both caught this way.
  # Runs AFTER vendoring: a file:../ sibling survives git-archive only as a dangling symlink,
  # which [ -d ] rejects. A subpath import is reduced to its PACKAGE name; scoped keeps two parts.
  - name: every direct import is declared
    run: |
      rc=0
      for spec in $(grep -hoE "from '[a-z0-9@][^']*'" index.js lib/*.js \
                    | sed -E "s/.*from '([^']*)'/\1/" | sort -u); do
        case "$spec" in
          @*) m=$(echo "$spec" | cut -d/ -f1,2) ;;
          *)  m=$(echo "$spec" | cut -d/ -f1) ;;
        esac
        # Check the DECLARATION, not the disk. `[ -d node_modules/$m ]` passes for anything hoisted
        # in as a transitive dep — which is exactly the bug this guard exists to catch: it resolves
        # locally, then the --production closure drops it and the daemon crash-loops MODULE_NOT_FOUND.
        # bare-os was undeclared here and the old check waved it through (found 2026-08-05).
        if node -e "const d=require('./package.json');process.exit(({...d.dependencies,...d.devDependencies})['$m']?0:1)"; then echo "  ok: $m"
        else echo "  UNDECLARED: $m (from '$spec') — resolves on disk but is not in package.json"; rc=1; fi
      done
      exit $rc
    timeout: 60

  # Tests run AFTER vendoring, against the exact tree that is about to be published.
  #
  # ⛔ They used to run BEFORE vendoring, on the claim that "the suites use fakes and import no
  # sibling". That stopped being true: the suites import anproto, favela-peer and favela-peer's test
  # helpers, which in the git-archive checkout are dangling file: symlinks until the vendor step. So
  # every build failed here (MODULE_NOT_FOUND: anproto), and a failed build records no version. That
  # is why no CI build of ap-bridge landed between 2026-08-18 and 2026-10-12. Found by dry-running
  # these steps on a git-archive export.
  #
  # ⛔ AND npm MUST NOT TOUCH THIS TREE NOW. `npm install brittle` re-resolves and deletes
  # hand-vendored directories (it reported "removed 1194 packages"). So brittle is installed into a
  # SEPARATE prefix, only the packages this tree lacks are copied in, the suite runs with the TREE'S
  # copy of brittle (brittle-bare runs tests in-process, so runner and tests must share one instance),
  # and exactly those added packages are removed again. The published closure is byte-for-byte what
  # it was before this step.
  #
  # Every REGISTRY devDependency is installed this way at its declared range, not just brittle: the
  # suites also import keet-identity-key, which `npm install --production` never installs. (file:
  # devDependencies are siblings, and the vendor step has already copied those in.)
  - name: tests (offline, against the vendored closure)
    run: |
      B=$(mktemp -d)
      SPECS=$(node -p 'Object.entries(require("./package.json").devDependencies||{}).filter(([,v])=>!String(v).startsWith("file:")).map(([k,v])=>k+"@"+v).join(" ")')
      echo "test-only devDependencies: $SPECS"
      npm install --prefix "$B" --no-audit --no-fund --no-save $SPECS >/dev/null 2>&1 || { echo "could not install: $SPECS"; exit 1; }
      ADDED=""
      for p in $(cd "$B/node_modules" && ls -A | grep -v -E '^\.'); do
        case "$p" in
          @*) for q in $(ls -A "$B/node_modules/$p"); do
                if [ ! -e "node_modules/$p/$q" ]; then mkdir -p "node_modules/$p" && cp -r "$B/node_modules/$p/$q" "node_modules/$p/$q" && ADDED="$ADDED $p/$q"; fi
              done ;;
          *)  if [ ! -e "node_modules/$p" ]; then cp -r "$B/node_modules/$p" "node_modules/$p" && ADDED="$ADDED $p"; fi ;;
        esac
      done
      echo "overlaid for the test run only:$ADDED"
      bare node_modules/brittle/bin/bare.js test/*.test.js; rc=$?
      for p in $ADDED; do rm -rf "node_modules/$p"; done
      rm -rf "$B"
      exit $rc
    timeout: 300

  - name: pgh publish (content-addressed -> installable app)
    run: pgh publish . --accept-conflict "automated CI publish; dep version-splits inherited from per-app lockfiles across the pgh ecosystem"
    timeout: 300