Create Index.html #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |