Skip to content

Development

This guide covers local development setup, project structure, and contribution guidelines.

Prerequisites

Tool Version Notes
Node.js 24.x (recommended) or 26.x Node 24 LTS is the project default (.nvmrc) and Docker runtime (node:24-bookworm-slim). Node 26 is also supported, but is currently not LTS
npm ≥ 11.19.0 Required for the package-specific install-script policy; current Node 24 LTS releases include a compatible npm version
git any recent Used at runtime for cloning/pulling repos
GitHub CLI (gh) ≥ 2.40 Only needed for GitHub backups
Azure CLI (az) + azure-devops ext. ≥ 2.50 Only needed for Azure DevOps backups
openssl 1.1.1 or newer To generate MASTER_KEY and SMTP TLS test certificates (req -addext)

Optional:

  • GitLab CLI (glab) — only used for boot probe and debugging; the REST API handles discovery
  • A throwaway PAT for your target provider
  • A test SMTP account (e.g., Mailtrap, Mailpit)

First-Time Setup

git clone https://github.com/TobiHochZwei/GitEcho.git
cd GitEcho

If you use nvm, select the project default before installing dependencies:

nvm install
nvm use

nvm is optional; an existing Node 24.x or 26.x installation also works. Install the locked dependencies and create local mount points:

npm ci

# Create local mount points
mkdir -p .dev/{config,data,backups}

better-sqlite3 13 uses Node-API and ships prebuilt binaries in its npm package for supported platforms, including macOS and Linux x64/arm64, rather than separate binaries for each Node major's V8 ABI. After switching runtimes, run npm ci to keep the full dependency installation consistent. If a binary cannot load, check platform/libc compatibility and the upstream source-build instructions; an automatic source build is not guaranteed. The project declares Node 24.x and 26.x support; npm normally warns for other versions without automatically switching your runtime.

The package-specific allowScripts policy disables only better-sqlite3 install scripts. npm can otherwise infer an unnecessary node-gyp rebuild from the package's binding.gyp, even though version 13 includes ready-to-use binaries. This avoids a compiler requirement in the slim image without disabling other dependencies' scripts globally.

Create .env.local (loaded by npm run dev / npm run worker:dev via Node's --env-file-if-exists flag; ignored by git):

cat > .env.local <<'EOF'
DATA_DIR=./.dev/data
CONFIG_DIR=./.dev/config
BACKUPS_DIR=./.dev/backups
BACKUP_MODE=option1
CRON_SCHEDULE=0 2 * * *
MASTER_KEY=
EOF

Generate a 32-byte (64 hex char) MASTER_KEY and append it:

echo "MASTER_KEY=$(openssl rand -hex 32)" >> .env.local

Keep this key safe — losing it makes every encrypted secret unrecoverable. In production, the same value must be passed to the container via the MASTER_KEY env var in docker-compose.yml (or docker run -e MASTER_KEY=…). .env.local is dev-only and never baked into the image.

Running in Development

GitEcho has two processes — run them in separate terminals:

Terminal 1 — Web UI

npm run dev

Serves the UI at http://localhost:3000 with hot module replacement.

On first boot, GitEcho bootstraps an admin / admin account. To reset during development, delete .dev/config/secrets.json and reload.

Terminal 2 — Background Worker

npm run worker:dev

Runs worker/index.ts directly through tsx. To trigger an immediate backup:

CRON_SCHEDULE='*/1 * * * *' npm run worker:dev   # every minute

Or use the Settings → General → Run backup button in the UI.

Production-Style Local Build

npm run build
npm start         # web server on port 3000
npm run worker    # background scheduler

Static Checks

npm test              # isolated unit and regression tests
npx astro check       # TypeScript + Astro template diagnostics
npm run build         # full production build (Astro + worker via esbuild)

Run all three checks before opening a PR. Database tests use temporary files, and SMTP tests use synthetic credentials and local test transports/servers rather than sending external email. The SMTP TLS fixtures also require openssl.

Linux and Container Validation

The Validate workflow (.github/workflows/validate.yml) runs on main pushes, pull requests, and manual dispatch. Its native Linux AMD64/ARM64 matrix covers Node 24 and 26, tests, Astro diagnostics, builds, the dependency tree, and npm audit.

The Node 24 jobs additionally build and smoke-test the production image on both architectures. To run the container check locally with Docker installed:

docker build -t gitecho:validation .
bash scripts/test-container.sh gitecho:validation

The smoke test starts the actual entrypoint with a synthetic encryption key and disposable data. Networking and scheduled backups are disabled. It verifies the login page, unprivileged processes, native SQLite, and in-memory mail generation without reading your configuration or sending mail.

This workflow never publishes an image. The separate Build and publish Docker image workflow publishes to GHCR on release-tag pushes or manual dispatch; only tag a commit after its validation passes.

Project Layout

src/
  layouts/Layout.astro          AdminLTE 4 shell (sidebar + topbar + dark mode)
  middleware.ts                 Session-cookie auth + Origin-based CSRF
  components/                   Reusable Astro components
  scripts/                      Client-side TS bundled by Astro
    theme.ts                    Dark-mode toggle
    sidebar.ts                  Sidebar collapse + treeview
    toasts.ts                   Bootstrap toast helper
    format.ts                   Shared formatters
  lib/
    config.ts                   Layered loader (env < settings < secrets)
    secrets.ts                  AES-256-GCM helpers
    settings.ts                 settings.json + secrets.json read/write
    backup-lock.ts              Cross-process file mutex
    database.ts                 SQLite schema + CRUD
    stats.ts                    Extended dashboard stats + storage usage
    scheduler.ts                node-cron entry point
    logger.ts                   Structured JSONL logger
    backup/engine.ts            The actual backup runner
    plugins/
      register.ts               Plugin registration
      interface.ts              ProviderPlugin interface
      github.ts                 GitHub provider
      azuredevops.ts            Azure DevOps provider
      gitlab.ts                 GitLab provider
    repos-file.ts               repos.txt parser/writer
  pages/
    index.astro                 Dashboard
    repos.astro                 Repository list
    runs.astro                  Backup runs
    logs.astro                  Log viewer
    runs/[id].astro             Per-run detail
    browse/[...path].astro      File browser (option1)
    zips/[...path].astro        ZIP archive list (option2/3)
    settings/                   Settings UI pages
    api/                        JSON endpoints
worker/index.ts                 Worker process entry
build-worker.mjs                esbuild bundle for the worker

Plugin Architecture

Each provider implements the ProviderPlugin interface:

interface ProviderPlugin {
  readonly name: string;
  readonly displayName: string;
  isConfigured(): boolean;
  authenticate(): Promise<boolean>;
  listRepositories(): Promise<RepositoryInfo[]>;
  cloneRepository(repoUrl: string, targetDir: string): Promise<void>;
  pullRepository(repoDir: string): Promise<void>;
  getAuthenticatedUrl(repoUrl: string): string;
}

Plugins are registered in src/lib/plugins/register.ts and accessed via the singleton PluginRegistry.

Database Migrations

GitEcho uses SQLite with a versioned migration system tracked by PRAGMA user_version.

Adding a Migration

Append a function to the MIGRATIONS array in src/lib/database.ts:

const MIGRATIONS: ReadonlyArray<(instance: DatabaseInstance) => void> = [
  // v0 → v1
  (instance) => instance.exec(`CREATE INDEX idx_items_run ON backup_items(run_id)`),
  // v1 → v2
  (instance) => instance.exec(`ALTER TABLE repositories ADD COLUMN labels TEXT`),
];

Rules:

  • Append-only — never edit, reorder, or delete a shipped migration
  • Don't duplicate what SCHEMA or legacy helpers already do
  • One concern per migration — keep rollbacks meaningful
  • Update SCHEMA in the same PR so fresh installs get the new shape directly

Destructive Changes

SQLite can't DROP COLUMN portably. Use the 4-step recipe:

(instance) => instance.exec(`
  CREATE TABLE backup_items_new (...new shape...);
  INSERT INTO backup_items_new (...) SELECT ... FROM backup_items;
  DROP TABLE backup_items;
  ALTER TABLE backup_items_new RENAME TO backup_items;
`);

UI / Theme Notes

  • Built on AdminLTE 4 (Bootstrap 5)
  • Dark mode via Bootstrap 5's data-bs-theme on <html>, persisted in localStorage
  • Toasts via window.gitechoToast(message, variant) — variants: success | danger | warning | info
  • Adding a sidebar item: edit src/components/Sidebar.astro
  • Adding a dashboard widget: extend src/lib/stats.ts, add a <SmallBox> or <Card> to index.astro
  • AdminLTE version: pinned to admin-lte@4.0.0-rc7