Skip to content

Create Index.html

Create Index.html #54

Workflow file for this run

name: πŸ§ͺ CI β€” Lint & Health
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ci:
name: Node ${{ matrix.node }} on ubuntu-latest
runs-on: ubuntu-latest
strategy:
matrix:
node: ['20', '22']
fail-fast: false
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🟒 Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
# ── Shell syntax checks ──────────────────────────────────────────────────
- name: 🐚 Shell syntax check (bash -n)
run: |
set -euo pipefail
FAIL=0
for f in \
setup.sh \
setup-termux-widget.sh \
install-termux.sh \
update.sh \
start.sh \
doctor.sh \
bin/isotope; do
if [ -f "$f" ]; then
if bash -n "$f" 2>/dev/null; then
echo "βœ… $f"
else
echo "❌ $f β€” syntax error"
bash -n "$f" || true
FAIL=1
fi
else
echo "⚠️ $f β€” not found (skipping)"
fi
done
[ "$FAIL" -eq 0 ] || exit 1
- name: πŸ” ShellCheck
run: |
set -euo pipefail
if ! command -v shellcheck >/dev/null 2>&1; then
echo "Installing shellcheck..."
sudo apt-get install -y shellcheck >/dev/null 2>&1
fi
echo "shellcheck $(shellcheck --version | head -2 | tail -1)"
FAIL=0
for f in \
setup.sh \
setup-termux-widget.sh \
install-termux.sh \
update.sh \
start.sh \
doctor.sh \
bin/isotope; do
if [ -f "$f" ]; then
# SC1090: can't follow sourced file β€” expected in this codebase
# SC1091: not following include β€” expected
# SC2148: no shebang for sourced files β€” not applicable
if shellcheck -x -e SC1090,SC1091 "$f"; then
echo "βœ… $f"
else
echo "⚠️ $f β€” shellcheck warnings (non-fatal for now)"
fi
else
echo "⚠️ $f β€” not found (skipping)"
fi
done
# ShellCheck is advisory in CI for now β€” uncomment to make it block:
# [ "$FAIL" -eq 0 ] || exit 1
# ── Node syntax ──────────────────────────────────────────────────────────
- name: βœ… Node.js syntax checks
run: |
set -euo pipefail
files=(
server.mjs
server/backup-manager.mjs
public/sync/backup-normalizer.js
public/sync/local-data-adapter.js
scripts/storage-backup-lib.mjs
scripts/validate-backup-files.mjs
scripts/repair-user-backup.mjs
scripts/prove-new-browser-restore.mjs
scripts/validate-storage-cleanup.mjs
scripts/verify-supabase-security.mjs
scripts/validate-docs.mjs
scripts/compare-remote-assets.mjs
)
for f in "${files[@]}"; do
node --check "$f"
echo "βœ… $f"
done
- name: πŸ§ͺ Backup normalizer safety smoke
run: |
set -euo pipefail
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import {
normalizeAnyBackup,
isBackupRich,
isBackupEmpty,
compareBackupCandidates,
mergeBackupData,
} from './public/sync/backup-normalizer.js';
const rich = normalizeAnyBackup({
version: 1,
source: 'isotopeai',
exportedAt: '2026-06-01T00:00:00.000Z',
data: {
profile: { name: 'rich' },
timerState: null,
tasks: [{ id: 'task-1', updatedAt: '2026-06-01T00:00:00.000Z' }],
sessions: [{ id: 'session-1', updatedAt: '2026-06-01T00:00:00.000Z' }],
subjects: [{ id: 'subject-1', updatedAt: '2026-06-01T00:00:00.000Z' }],
habits: [],
dailyLogs: [],
tests: [],
exams: [],
mockTests: [],
},
}, { source_path: 'imports/latest.json' });
const emptyNewer = normalizeAnyBackup({
schema_version: 1,
exported_at: '2026-06-11T00:00:00.000Z',
profile_data: { name: 'empty' },
backup_data: {
profile: { name: 'empty' },
tasks: [],
sessions: [],
subjects: [],
habits: [],
dailyLogs: [],
tests: [],
exams: [],
mockTests: [],
},
}, { source_path: 'cloud-snapshot/latest.json' });
assert.equal(isBackupRich(rich), true);
assert.equal(isBackupEmpty(emptyNewer), true);
assert.equal(compareBackupCandidates(
{ normalized: rich, exported_at: rich.exported_at, size_bytes: rich.size },
{ normalized: emptyNewer, exported_at: emptyNewer.exported_at, size_bytes: emptyNewer.size },
), 1, 'rich backup must beat newer empty backup');
const merged = mergeBackupData(emptyNewer, rich);
assert.equal(merged.tasks.length, 1);
assert.equal(merged.sessions.length, 1);
assert.equal(merged.subjects.length, 1);
console.log('βœ… backup normalizer protects rich data from empty overwrite');
NODE
# ── Required files ───────────────────────────────────────────────────────
- name: πŸ“ Required files present
run: |
set -euo pipefail
FAIL=0
REQUIRED=(
server.mjs
package.json
setup.sh
setup.bat
install.ps1
install-termux.sh
setup-termux-widget.sh
update.sh
update.bat
start.sh
start.bat
doctor.sh
doctor.bat
bin/isotope
bin/isotope.bat
isotope-complete.sql
community-patch-v4.sql
sql/006_security_policy_cleanup.sql
sql/backup_manifests.sql
sql/verify-security.sql
.env.example
README.md
ADMIN.md
CHANGELOG.md
TERMUX_WIDGET.md
docs/sync-system.md
docs/storage-backup-system.md
docs/supabase-connection-map.md
public/sync/backup-normalizer.js
public/sync/local-data-adapter.js
scripts/validate-backup-files.mjs
scripts/repair-user-backup.mjs
scripts/prove-new-browser-restore.mjs
scripts/validate-storage-cleanup.mjs
scripts/verify-supabase-security.mjs
scripts/compare-remote-assets.mjs
server/backup-manager.mjs
)
for f in "${REQUIRED[@]}"; do
if [ -f "$f" ]; then
echo "βœ… $f"
else
echo "❌ $f β€” MISSING"
FAIL=1
fi
done
[ "$FAIL" -eq 0 ] || exit 1
# ── No hardcoded secrets ─────────────────────────────────────────────────
- name: πŸ”’ No hardcoded secrets
run: |
set -euo pipefail
if git ls-files --error-unmatch .env >/dev/null 2>&1; then
echo "❌ .env is tracked; remove it before release"
exit 1
fi
if git grep -nE 'SUPABASE_SERVICE_ROLE_KEY[[:space:]]*=[[:space:]]*eyJ|SUPABASE_ACCESS_TOKEN[[:space:]]*=[[:space:]]*sbp_|ADMIN_SECRET[[:space:]]*=[[:space:]]*[A-Za-z0-9_-]{24,}' -- \
':!*.md' ':!.env.example' ':!public/assets/**'; then
echo "❌ Hardcoded private secret detected!"
exit 1
fi
echo "βœ… No hardcoded secrets"
# ── Schema stats ─────────────────────────────────────────────────────────
- name: πŸ“‹ Schema file stats
run: |
set -euo pipefail
for f in isotope-complete.sql community-patch-v4.sql sql/006_security_policy_cleanup.sql sql/backup_manifests.sql sql/verify-security.sql performance-patch.sql; do
if [ -f "$f" ]; then
echo "=== $f ==="
echo " Size : $(wc -c < "$f") bytes"
echo " Lines : $(wc -l < "$f")"
echo " Tables : $(awk '/CREATE TABLE/{n++} END{print n+0}' "$f")"
echo " Funcs : $(awk '/CREATE OR REPLACE FUNCTION/{n++} END{print n+0}' "$f")"
echo " Policies: $(awk '/CREATE POLICY/{n++} END{print n+0}' "$f")"
else
echo "⚠️ $f not found β€” skipping"
fi
done
# ── Termux simulation ─────────────────────────────────────────────────────
- name: πŸ“± Termux simulation β€” platform detection
run: |
set -euo pipefail
echo "Checking Termux scripts without running installers..."
bash -n setup.sh
bash -n install-termux.sh
bash -n setup-termux-widget.sh
bash -n bin/isotope
grep -q 'TERMUX_VERSION' setup.sh install-termux.sh
echo "βœ… Termux scripts parse and contain Termux detection"
# ── Smoke test ────────────────────────────────────────────────────────────
- name: πŸš€ Smoke test β€” server starts and responds
run: |
set -uo pipefail
SUPABASE_URL=https://placeholder.supabase.co \
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYW5vbiJ9.signature \
SESSION_SECRET=ci_smoke_test_secret_not_real \
ADMIN_SECRET=ci_admin_placeholder \
NODE_ENV=test \
PORT=3099 \
node server.mjs &
SERVER_PID=$!
echo "Server PID: $SERVER_PID β€” polling /__isotope/ping..."
STATUS="000"
for i in $(seq 1 40); do
sleep 0.5
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
http://127.0.0.1:3099/__isotope/ping 2>/dev/null || echo "000")
if [ "$STATUS" = "200" ]; then
echo " HTTP $STATUS after ${i} attempts"
break
fi
printf ' Attempt %d: waiting...\n' "$i"
done
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
echo "Final health status: $STATUS"
if [ "$STATUS" != "200" ]; then
echo "❌ Server ping did not return 200 (got: $STATUS)"
exit 1
fi
echo "βœ… Server started and ping returned HTTP 200"
# ── Release zip simulation ────────────────────────────────────────────────
- name: πŸ“¦ Release zip β€” build + verify extract
run: |
set -euo pipefail
VERSION="$(node -e "process.stdout.write(require('./package.json').version)")"
echo "Building release zip for v$VERSION..."
zip -qr "isotope-v${VERSION}-ci-test.zip" . \
--exclude ".git/*" \
--exclude "node_modules/*" \
--exclude ".env" \
--exclude "*.log" \
--exclude ".github/*" \
--exclude "screenshots/*" \
--exclude "*.zip"
echo "Archive: $(wc -c < "isotope-v${VERSION}-ci-test.zip") bytes"
# Extract to a fresh dir and check required files are present
TMPDIR="$(mktemp -d)"
unzip -q "isotope-v${VERSION}-ci-test.zip" -d "$TMPDIR"
echo "Verifying extracted release..."
FAIL=0
for f in server.mjs package.json setup.sh install-termux.sh bin/isotope .env.example public/sync/backup-normalizer.js public/sync/local-data-adapter.js server/backup-manager.mjs scripts/prove-new-browser-restore.mjs sql/backup_manifests.sql docs/sync-system.md docs/storage-backup-system.md docs/supabase-connection-map.md; do
if [ -f "$TMPDIR/$f" ]; then
echo " βœ… $f"
else
echo " ❌ $f β€” missing from zip"
FAIL=1
fi
done
rm -rf "$TMPDIR" "isotope-v${VERSION}-ci-test.zip"
[ "$FAIL" -eq 0 ] || exit 1
echo "βœ… Release zip contains all required files"
# ── Docs validation ──────────────────────────────────────────────────────
- name: πŸ“ Docs + README validation
run: |
set -euo pipefail
node scripts/validate-docs.mjs || {
echo "⚠️ Docs validation had errors β€” see output above"
exit 1
}
# ── Upload logs on failure ────────────────────────────────────────────────
- name: πŸ“€ Upload CI logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: ci-logs-node${{ matrix.node }}-${{ github.run_id }}
path: |
~/.isotope/logs/
/tmp/isotope-ci-*.log
if-no-files-found: ignore
retention-days: 7