SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%

fix: async git in e2e (in-process server deadlock), vitest config, ngrok token resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed yesterday (Aug 10, 2026) parent faa190e

Showing 4 changed files with +66 and −13

modified deploy/ecosystem.config.cjs +3 −1
@@ -41,7 +41,9 @@ module.exports = {
41 41 name: 'spbgit-tunnel',
42 42 cwd: APP_DIR,
43 43 script: 'ngrok',
44 args: 'start --config deploy/ngrok.yml spbgit',
44 + // SPBGIT_NGROK_CONFIG lets the host point at a resolved config that
45 + // carries the real authtoken (deploy/ngrok.yml is the tracked template).
46 + args: `start --config ${process.env.SPBGIT_NGROK_CONFIG || 'deploy/ngrok.yml'} spbgit`,
45 47 interpreter: 'none',
46 48 autorestart: true,
47 49 max_restarts: 50,
modified deploy/setup-m3u96a.sh +27 −3
@@ -113,13 +113,37 @@ else
113 113 fi
114 114
115 115 # ── 6. ngrok auth + pm2 ──────────────────────────────────────────
116 if [ -z "${NGROK_AUTHTOKEN:-}" ] && ! ngrok config check >/dev/null 2>&1; then
117 printf '\033[1;33m! NGROK_AUTHTOKEN not set and no ngrok config found.\033[0m\n'
116 +# Resolve the authtoken: env first, then the agent's default config file.
117 +# A resolved config (real token, chmod 600) is written OUTSIDE the repo and
118 +# handed to pm2 via SPBGIT_NGROK_CONFIG — deploy/ngrok.yml stays a template.
119 +NGROK_DEFAULT_CFG="$HOME/Library/Application Support/ngrok/ngrok.yml"
120 +[ -f "$NGROK_DEFAULT_CFG" ] || NGROK_DEFAULT_CFG="$HOME/.config/ngrok/ngrok.yml"
121 +AUTHTOKEN="${NGROK_AUTHTOKEN:-}"
122 +if [ -z "$AUTHTOKEN" ] && [ -f "$NGROK_DEFAULT_CFG" ]; then
123 + AUTHTOKEN="$(grep -E '^[[:space:]]*authtoken:' "$NGROK_DEFAULT_CFG" | head -1 | awk '{print $2}')"
124 +fi
125 +RESOLVED_CFG="$PREFIX/spbgit/ngrok.resolved.yml"
126 +if [ -n "$AUTHTOKEN" ]; then
127 + cat > "$RESOLVED_CFG" <<NGROK
128 +version: 3
129 +agent:
130 + authtoken: $AUTHTOKEN
131 +endpoints:
132 + - name: spbgit
133 + url: https://git.spboucher.ai
134 + upstream:
135 + url: http://127.0.0.1:7420
136 +NGROK
137 + chmod 600 "$RESOLVED_CFG"
138 + export SPBGIT_NGROK_CONFIG="$RESOLVED_CFG"
139 + ok "ngrok config resolved → $RESOLVED_CFG"
140 +else
141 + printf '\033[1;33m! No ngrok authtoken found (env or agent config).\033[0m\n'
118 142 printf ' Run: ngrok config add-authtoken <token> (or export NGROK_AUTHTOKEN)\n'
119 143 fi
120 144
121 145 say "starting pm2 apps"
122 SPBGIT_LOG_DIR="$LOG_DIR" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null
146 +SPBGIT_LOG_DIR="$LOG_DIR" SPBGIT_NGROK_CONFIG="${SPBGIT_NGROK_CONFIG:-}" pm2 start deploy/ecosystem.config.cjs --update-env >/dev/null
123 147 pm2 save >/dev/null
124 148 ok "pm2 apps started (spbgit-server, spbgit-tunnel) and saved"
125 149 printf ' To survive reboots, run once: \033[1mpm2 startup\033[0m (follow its instructions)\n'
modified test/e2e/roundtrip.test.mjs +14 −9
@@ -14,8 +14,13 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
14 14 import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
15 15 import { tmpdir } from 'node:os';
16 16 import { join } from 'node:path';
17 import { execFileSync } from 'node:child_process';
17 +import { execFile } from 'node:child_process';
18 +import { promisify } from 'node:util';
18 19 import net from 'node:net';
20 +
21 +// The server runs inside this very process — git must be spawned async or
22 +// the event loop blocks and the push deadlocks against our own server.
23 +const execFileAsync = promisify(execFile);
19 24 import { loadConfig } from '../../src/config.mjs';
20 25 import { buildServer } from '../../src/server.mjs';
21 26 import { TokenStore } from '../../src/auth/token.mjs';
@@ -116,24 +121,24 @@ describe('full round trip', () => {
116 121 expect(res.headers.get('www-authenticate')).toContain('Basic');
117 122 });
118 123
119 it('pushes with a real git client and PAT', () => {
124 + it('pushes with a real git client and PAT', async () => {
120 125 const work = join(root, 'work');
121 126 mkdirSync(work, { recursive: true });
122 execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: work, env: GIT_ENV });
127 + await execFileAsync('git', ['init', '-q', '-b', 'main'], { cwd: work, env: GIT_ENV });
123 128 writeFileSync(join(work, 'README.md'), '# E2E\n\n![badge](https://img.shields.io/badge/e2e-pass-green)\n\n```js\nconst ok = true;\n```\n');
124 129 writeFileSync(join(work, 'main.go'), 'package main\n\nfunc main() {}\n');
125 execFileSync('git', ['add', '-A'], { cwd: work, env: GIT_ENV });
126 execFileSync('git', ['commit', '-qm', 'feat: e2e commit'], { cwd: work, env: GIT_ENV });
130 + await execFileAsync('git', ['add', '-A'], { cwd: work, env: GIT_ENV });
131 + await execFileAsync('git', ['commit', '-qm', 'feat: e2e commit'], { cwd: work, env: GIT_ENV });
127 132 const url = new URL(`${base}/e2e-demo.git`);
128 133 url.username = 'spb';
129 134 url.password = token;
130 execFileSync('git', ['push', '-q', url.href, 'main'], { cwd: work, env: GIT_ENV });
135 + await execFileAsync('git', ['push', '-q', url.href, 'main'], { cwd: work, env: GIT_ENV });
131 136 }, 60000);
132 137
133 it('anonymous clone works', () => {
138 + it('anonymous clone works', async () => {
134 139 const dest = join(root, 'clone');
135 execFileSync('git', ['clone', '-q', `${base}/e2e-demo.git`, dest], { env: GIT_ENV });
136 execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dest, env: GIT_ENV });
140 + await execFileAsync('git', ['clone', '-q', `${base}/e2e-demo.git`, dest], { env: GIT_ENV });
141 + await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: dest, env: GIT_ENV });
137 142 }, 60000);
138 143
139 144 it('API reflects the push (commits, language, stats)', async () => {
added vitest.config.mjs +22 −0
@@ -0,0 +1,22 @@
1 +/**
2 + * ─────────────────────────────────────────────
3 + * SPB Git — Personal Git Platform
4 + * ─────────────────────────────────────────────
5 + * Author : Simon-Pierre Boucher
6 + * Contact : contact@spboucher.ai
7 + * File : vitest.config.mjs
8 + * Purpose : Vitest configuration — single fork (e2e binds real ports)
9 + * License : MIT © Simon-Pierre Boucher
10 + * ─────────────────────────────────────────────
11 + */
12 +
13 +import { defineConfig } from 'vitest/config';
14 +
15 +export default defineConfig({
16 + test: {
17 + pool: 'forks',
18 + poolOptions: { forks: { singleFork: true } },
19 + testTimeout: 30000,
20 + hookTimeout: 120000,
21 + },
22 +});
23