* feat(ui): redesign dashboard and configuration screens
New settings design language built from the design/giteamirror.pen file:
cards with icon headers and status footers, header-level enable switches,
toggle switches instead of checkboxes, uppercase section titles, selection
tiles with icon chips and a check on the active option, segmented controls,
and an indigo accent. Implemented via shared primitives in
src/components/config/settings-ui.tsx and applied across:
- Automation: header switches, schedule card, one-line auto-mirror copy
with info tooltip, full-width Repository Cleanup card with Skip/Archive/
Delete tiles and dry run row
- Notifications: segmented provider picker (ntfy/Apprise/Gotify/Webhook),
events card with per-event switches
- Connections: GitHub/Gitea connection cards with token creation guide and
field helpers, Repository Selection and Mirror Content cards covering
every mirror option, Organization Structure card with strategy tiles,
destructive update protection tiles (BETA label removed)
- Authentication: sign-in methods status card, identity providers restyle
- Dashboard: flatter stat cards, icon panel headers, indigo view-all links
All existing state handling, autosave and API behavior is unchanged.
Light mode keeps working via theme tokens. README and website screenshots
regenerated, docs references to renamed cards updated.
* fix(ui): design polish pass from local review
- Recent Activity rows get status icon circles (check, sync, sparkles, alert)
- Connections tab restructured: connection cards share a stretched grid row
so GitHub and Gitea stay equal height; Mirror Content moved to the right
column; forms split into placeable cards via a part prop
- Token guide panel: link moved to header as icon, larger text; redundant
card footers removed (scopes line, test-connection hint)
- Repository Selection gains a footer note; retention explanation moved
below the selector
- Authentication tab matches the design: side-by-side cards, row dividers,
disabled state-reflecting switches with info hints, footers; SSO dialog
restyled (segmented protocol tabs, field labels, indigo primary)
- Import GitHub Data button is the indigo primary; disabled state is muted
- Time format menu redesigned (locale pill, live examples, live clock in
the trigger); theme switcher moved to sidebar as icon segmented control,
system preference now persists correctly; legacy ModeToggle removed
- Automation timezone pill no longer shows stored legacy UTC as a choice
- Config tab bar wraps 2x2 on narrow screens instead of overflowing
- README, website and PR screenshots regenerated
The check added in #350 only echoed a message (with backticks that bash
executed as command substitution, mangling it) and never failed the job,
so a stale bun.nix would still pass CI. Fail fast right after the
regenerate step instead, with a GitHub error annotation and the diffstat.
bun.nix (last regenerated in #298) drifted from bun.lock after the dependency updates in #348, leaving the Nix flake build broken: 254 packages missing and 195 stale entries. Regenerated with bun2nix; all 929 entries now match bun.lock exactly.
Also adds a CI step that detects drift after the regenerate step (made blocking in a follow-up commit).
Thanks @Cyberboss.
Resolves#306. SSO sign-in via OIDC (Authentik / Keycloak / etc.) now links the
SSO identity to an existing email/password admin instead of bouncing to /login
with `?error=UNKNOWN`. Account-linking is gated on the operator-supplied
**Domain** field — cross-domain claims from a compromised IdP are refused.
Also bundles the deprecated `oidcProvider` → `@better-auth/oauth-provider`
migration. **Operators using the OAuth-provider feature must rotate registered
client secrets after upgrade** (legacy plaintext → hashed storage; see the
0012 migration notes).
Verified end-to-end on the pr-307 image against a real Authentik instance:
SSO login lands on the dashboard, `accounts` table gets both `credential` and
`authentik` rows for the same user. See PR description for full details.
Tags are mutable. A compromised maintainer (or a maintainer's compromised
machine) can force-move v-tags to point at malicious commits, and any workflow
using `@vN` picks up the malicious code on its next run — see the recent
`actions-cool/issues-helper` / `maintain-one-comment` incident exfiltrating
credentials from `Runner.Worker` memory.
This commit pins every third-party action in the two workflows that handle
secrets (GHCR push, Docker Hub login, Scout token) to immutable 40-char SHAs,
with a trailing comment naming the release version for readability. SHAs are
the latest released tag at time of pin.
The two DeterminateSystems actions were on `@main` — a *branch* ref that moves
on every push, materially worse than a tag — and are now pinned to the latest
release SHAs (v22 / v13).
First-party `actions/*` and `github/codeql-action` are left on tags for now;
they're a separate, lower-risk follow-up.
CI was on 1.3.10 while the Dockerfile runtime moved to 1.3.12 in v3.15.2,
so we were testing against an older runtime than we shipped. Align both
on 1.3.13 (latest stable). May also resolve the intermittent --coverage
instrumentation flake observed on 1.3.10 against http-client.ts.
The Nix build has been failing since v3.9.6 because bun.nix fell out
of sync with bun.lock. During the sandboxed build bun install cannot
fetch missing packages, causing ConnectionRefused errors.
- Add bun2nix regeneration step before nix build in CI
- Trigger workflow on bun.lock and package.json changes
- Update flake.nix version from 3.9.6 to 3.14.1
SQLite rejects ALTER TABLE ADD COLUMN with expression defaults like
DEFAULT (unixepoch()), which Drizzle-kit generated for the imported_at
column. This broke upgrades from v3.12.x to v3.13.0 (#228, #229).
Changes:
- Rewrite migration 0009 using table-recreation pattern (CREATE, INSERT
SELECT, DROP, RENAME) instead of ALTER TABLE
- Add migration validation script with SQLite-specific lint rules that
catch known invalid patterns before they ship
- Add upgrade-path testing with seeded data and verification fixtures
- Add runtime repair for users whose migration record may be stale
- Add explicit migration validation step to CI workflow
Fixes#228Fixes#229
Bun 1.3.9 crashes with a segfault on CPUs without AVX support due to a
WASM IPInt bug (oven-sh/bun#27340), fixed in 1.3.10 via oven-sh/bun#26922.
- Bump Bun from 1.3.9 to 1.3.10 in Dockerfile, CI workflows, and packageManager
- Skip env config script when no GitHub/Gitea env vars are set
- Make startup scripts (env-config, recovery, repair) fault-tolerant so
a crash in a non-critical script doesn't abort the entrypoint via set -e
* feat: add autoMirrorStarred toggle for selective starred repo mirroring (#205)
Add `githubConfig.autoMirrorStarred` (default: false) to control whether
starred repos are included in automatic mirroring operations. Manual
per-repo actions always work regardless of this toggle.
Bug fixes:
- Cleanup service no longer orphans starred repos when includeStarred is
disabled (prevents data loss)
- First-boot auto-start now gates initial mirror behind autoMirror config
(previously mirrored everything unconditionally)
- "Mirror All" button now respects autoMirrorStarred setting
- Bulk mirror and getAvailableActions now include pending-approval status
Changes span schema, config mapping, env loader, scheduler, cleanup
service, UI settings toggle, and repository components.
* fix: log activity when repos are auto-imported during scheduled sync
Auto-discovered repositories (including newly starred ones) were inserted
into the database without creating activity log entries, so they appeared
in the dashboard but not in the activity log.
* ci: set 10-minute timeout on all CI jobs
Add www/** to paths-ignore in astro-build-test, e2e-tests, and
nix-build workflows. docker-build and helm-test already use positive
path filters and were unaffected.
* feat: smart force-push protection with backup strategies (#187)
Replace blunt `backupBeforeSync` boolean with `backupStrategy` enum
offering four modes: disabled, always, on-force-push (default), and
block-on-force-push. This dramatically reduces backup storage for large
mirror collections by only creating snapshots when force-pushes are
actually detected.
Detection works by comparing branch SHAs between Gitea and GitHub APIs
before each sync — no git cloning required. Fail-open design ensures
detection errors never block sync.
Key changes:
- Add force-push detection module (branch SHA comparison via APIs)
- Add backup strategy resolver with backward-compat migration
- Add pending-approval repo status with approve/dismiss UI + API
- Add block-on-force-push mode requiring manual approval
- Fix checkAncestry to only treat 404 as confirmed force-push
(transient errors skip branch instead of false-positive blocking)
- Fix approve-sync to bypass detection gate (skipForcePushDetection)
- Fix backup execution to not be hard-gated by deprecated flag
- Persist backupStrategy through config-mapper round-trip
* fix: resolve four bugs in smart force-push protection
P0: Approve flow re-blocks itself — approve-sync now calls
syncGiteaRepoEnhanced with skipForcePushDetection: true so the
detection+block gate is bypassed on approved syncs.
P1: backupStrategy not persisted — added to both directions of the
config-mapper. Don't inject a default in the mapper; let
resolveBackupStrategy handle fallback so legacy backupBeforeSync
still works for E2E tests and existing configs.
P1: Backup hard-gated by deprecated backupBeforeSync — added force
flag to createPreSyncBundleBackup; strategy-driven callers and
approve-sync pass force: true to bypass the legacy guard.
P1: checkAncestry false positives — now only returns false for
404/422 (confirmed force-push). Transient errors (rate limits, 500s)
are rethrown so detectForcePush skips that branch (fail-open).
* test(e2e): migrate backup tests from backupBeforeSync to backupStrategy
Update E2E tests to use the new backupStrategy enum ("always",
"disabled") instead of the deprecated backupBeforeSync boolean.
* docs: add backup strategy UI screenshot
* refactor(ui): move Destructive Update Protection to GitHub config tab
Relocates the backup strategy section from GiteaConfigForm to
GitHubConfigForm since it protects against GitHub-side force-pushes.
Adds ShieldAlert icon to match other section header patterns.
* docs: add force-push protection documentation and Beta badge
Add docs/FORCE_PUSH_PROTECTION.md covering detection mechanism,
backup strategies, API usage, and troubleshooting. Link it from
README features list and support section. Mark the feature as Beta
in the UI with an outline badge.
* fix(ui): match Beta badge style to Git LFS badge
* fix(nix): ensure absolute bundle path in pre-sync backup (#203)
Use path.resolve() instead of conditional path.isAbsolute() check to
guarantee bundlePath is always absolute before passing to git -C. On
NixOS, relative paths were interpreted relative to the temp mirror
clone directory, causing "No such file or directory" errors.
Closes#203
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(nix): ensure absolute bundle path in pre-sync backup (#203)
Use path.resolve() instead of conditional path.isAbsolute() check to
guarantee bundlePath is always absolute before passing to git -C. On
NixOS, relative paths were interpreted relative to the temp mirror
clone directory, causing "No such file or directory" errors.
Extract resolveBackupPaths() for testability. Bump version to 3.10.1.
Closes#203
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: drop macos matrix and only run nix build on main/tags
- Remove macos-latest from Nix CI matrix (ubuntu-only)
- Only run `nix build` on main branch and version tags, skip on PRs
- `nix flake check` still runs on all PRs for validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add E2E testing infrastructure with fake GitHub, Playwright, and CI workflow
- Add fake GitHub API server (tests/e2e/fake-github-server.ts) with
management API for seeding test data
- Add Playwright E2E test suite covering full mirror workflow:
service health checks, user registration, config, sync, verify
- Add Docker Compose for E2E Gitea instance
- Add orchestrator script (run-e2e.sh) with cleanup
- Add GitHub Actions workflow (e2e-tests.yml) with Gitea service container
- Make GITHUB_API_URL configurable via env var for testing
- Add npm scripts: test:e2e, test:e2e:ci, test:e2e:keep, test:e2e:cleanup
* feat: add real git repos + backup config testing to E2E suite
- Create programmatic test git repos (create-test-repos.ts) with real
commits, branches (main, develop, feature/*), and tags (v1.0.0, v1.1.0)
- Add git-server container to docker-compose serving bare repos via
dumb HTTP protocol so Gitea can actually clone them
- Update fake GitHub server to emit reachable clone_url fields pointing
to the git-server container (configurable via GIT_SERVER_URL env var)
- Add management endpoint POST /___mgmt/set-clone-url for runtime config
- Update E2E spec with real mirroring verification:
* Verify repos appear in Gitea with actual content
* Check branches, tags, commits, file content
* Verify 4/4 repos mirrored successfully
- Add backup configuration test suite:
* Enable/disable backupBeforeSync config
* Toggle blockSyncOnBackupFailure
* Trigger re-sync with backup enabled and verify activities
* Verify config persistence across changes
- Update CI workflow to use docker compose (not service containers)
matching the local run-e2e.sh approach
- Update cleanup.sh for git-repos directory and git-server port
- All 22 tests passing with real git content verification
* refactor: split E2E tests into focused files + add force-push tests
Split the monolithic e2e.spec.ts (1335 lines) into 5 focused spec files
and a shared helpers module:
helpers.ts — constants, GiteaAPI, auth, saveConfig, utilities
01-health.spec.ts — service health checks (4 tests)
02-mirror-workflow.spec.ts — full first-mirror journey (8 tests)
03-backup.spec.ts — backup config toggling (6 tests)
04-force-push.spec.ts — force-push simulation & backup verification (9 tests)
05-sync-verification.spec.ts — dynamic repos, content integrity, reset (5 tests)
The force-push tests are the critical addition:
F0: Record original state (commit SHAs, file content)
F1: Rewrite source repo history (simulate force-push)
F2: Sync to Gitea WITHOUT backup
F3: Verify data loss — LICENSE file gone, README overwritten
F4: Restore source, re-mirror to clean state
F5: Enable backup, force-push again, sync through app
F6: Verify Gitea reflects the force-push
F7: Verify backup system was invoked (snapshot activities logged)
F8: Restore source repo for subsequent tests
Also added to helpers.ts:
- GiteaAPI.getBranch(), .getCommit(), .triggerMirrorSync()
- getRepositoryIds(), triggerMirrorJobs(), triggerSyncRepo()
All 32 tests passing.
* Try to fix actions
* Try to fix the other action
* Add debug info to check why e2e action is failing
* More debug info
* Even more debug info
* E2E fix attempt #1
* E2E fix attempt #2
* more debug again
* E2E fix attempt #3
* E2E fix attempt #4
* Remove a bunch of debug info
* Hopefully fix backup bug
* Force backups to succeed
* fix(nix): enable sandboxed builds with bun2nix
The Nix package was broken on Linux because `bun install` requires
network access, which is blocked by Nix sandboxing (enabled by default
on Linux).
This switches to bun2nix for dependency management:
- Add bun2nix flake input to pre-fetch all npm dependencies
- Generate bun.nix lockfile for reproducible dependency resolution
- Copy bun cache to writable location during build to avoid EACCES
errors from bunx writing to the read-only Nix store
- Add nanoid as an explicit dependency (was imported directly but only
available as a transitive dep, which breaks with isolated linker)
- Update CI workflow to perform a full sandboxed build
- Add bun2nix to devShell for easy lockfile regeneration
Closes#197
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(nix): create writable workdir for database access
The app uses process.cwd()/data for the database path, but when running
from the Nix store the cwd is read-only. Create a writable working
directory with symlinks to app files and a real data directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bun install requires network access which Nix sandbox blocks.
CI now validates flake structure and evaluates expressions only.
Full builds work locally with: nix build --option sandbox false
- Use DeterminateSystems/nix-installer-action for Nix installation
- Use DeterminateSystems/magic-nix-cache-action for caching (free, no setup)
- Update documentation to remove Cachix references
- Add nix branch to CI triggers
The previous name 'skipStarredIssues' was misleading as it now skips ALL
metadata (not just issues) for starred repositories. The new name
'starredCodeOnly' better reflects the actual behavior - mirroring only
source code for starred repos.
Changes:
- Renamed skipStarredIssues → starredCodeOnly in all files
- Updated UI label from "Don't fetch issues" to "Code-only mode"
- Updated description to clarify it skips ALL metadata types:
issues, PRs, labels, milestones, wiki, and releases
- Updated database schema, types, config mapper, and all components
- Updated Helm charts, CI configs, and documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added docker-entrypoint.sh to trigger paths
- Added drizzle/** for database migrations
- Added scripts/** for database management scripts
- Added src/** for source code changes
This ensures Docker images are rebuilt when critical runtime
files change, not just package dependencies.
## Problem
Forgejo 12.0+ rejects migration API calls with credentials embedded in URLs,
causing HTTP 422 errors when mirroring private GitHub repositories.
## Root Cause
Breaking security change in Forgejo 12.0 (July 2025) enforces credential
separation to prevent accidental exposure in logs/errors. Previous versions
(Forgejo 11.x, Gitea 1.x) accepted embedded credentials.
## Solution
- Use separate `auth_username` and `auth_token` fields instead of embedding
credentials in clone URLs
- Set `auth_username` to "oauth2" for GitHub token authentication
- Pass GitHub token via `auth_token` field
## Changes
- src/lib/gitea.ts:
- mirrorGithubRepoToGitea(): Use separate auth fields for private repos
- mirrorGitHubRepoToGiteaOrg(): Use separate auth fields for private repos
- .github/workflows/docker-build.yml:
- Enable PR image building and pushing to GHCR
- Tag PR images as pr-<number> for easy testing
- Add automated PR comment with image details and testing instructions
- Separate load step for security scanning
## Backward Compatibility
✅ Works with Forgejo 12.0+
✅ Works with Forgejo 11.x and earlier
✅ Works with Gitea 1.x
## Testing
Public repos: ✅ Working (no auth needed)
Private repos: ✅ Fixed (separate auth fields)
Fixes#102
- Add missing database fields (language, description, mirroredLocation, destinationOrg) to repository operations
- Add missing organization fields (publicRepositoryCount, privateRepositoryCount, forkRepositoryCount) to schema
- Update GitRepo interface to include all required database fields
- Fix GitHub data fetching functions to map all fields correctly
- Update all sync endpoints (main, repository, organization, scheduler) to handle new fields
This fixes the "SQLite query expected X values, received Y" error when importing
large numbers (4.6k+) of starred repositories by ensuring all database fields
are properly mapped from GitHub API responses through to database insertion.
- Update Bun version in CI to match local version (1.2.16)
- Add bunfig.toml with 5s test timeout to prevent hanging tests
- Mock setTimeout globally in test setup to avoid timing issues
- Add NODE_ENV check to skip delays during tests
- Fix missing exports in config-encryption mock
- Remove retryDelay in tests to ensure immediate execution
These changes ensure tests run consistently between local and CI environments