The repositories API returns each repository's destination, resolved the way the mirror does it, and the page filters on it. The Organization column shows the same value.
The desktop filter sits in the second filter row so the toolbar still fits at 1024 and 1280px.
Adds three issue options under the metadata settings, each with organization and repository overrides: a state filter (all, open only or closed only), copying comments, and copying images and attachments.
The state filter only decides which issues get created. Mirrored issues keep following GitHub, and changing the filter runs a full issue pass. Media removal covers GitHub uploads only.
Follow-up to #487: the owner, organization and Activity Log name filters share one compareNames (an Intl.Collator built once), so the Activity Log filter no longer puts capitalized names first.
The feed parsed job statuses with the repository status enum, which has no "warning", so one release filter warning turned GET /api/activities into a 500. It now parses with activityStatusEnum. Adds a route test with warning and mirrored rows.
A global Remove old release assets switch (PRUNE_OLD_RELEASE_ASSETS) deletes the attachments of mirrored releases ranked past each repository's asset limit on every sync. Releases, notes and tags stay. Off by default. A release whose attachments cannot be listed is left alone, and releases the listing already shows without attachments are skipped.
Closes#485
Owner and organization filter options are sorted with Intl.Collator, case and accent insensitive, instead of a plain sort that put "Zebra" before "apple".
- Dashboard, Repositories, Organizations, Storage and Activity Log keep what they last showed in this tab and refresh it in the background instead of starting from a skeleton. The cache is in memory and per user; nothing goes to browser storage.
- The Repositories page renders only the layout on screen, and the mobile cards and Storage rows are virtualized against the page scroll. With 3,007 repositories the page went from 124,174 elements to 880.
- The desktop table rows live in their own memoized component that owns the virtualizer, so a scroll renders only the rows coming into view. MarqueeText no longer forces a style recalculation on mount. With the CPU slowed 4x a scroll frame went from 30.7 ms to 12.6 ms.
- Refresh sizes measures every mirror on the destination now instead of at its next sync. It only reads the destination: one repository listing per owner, the single repository GET for a mirror the listing does not have, and the release listing of repositories that mirror releases. POST /api/storage/refresh runs it in the background; GET /api/storage reports its progress.
- Destination release listings stopped after 30 releases: Gitea and Forgejo ignore per_page and answer 30 a page. The retention pass, the release filter preview and the refresh now share one pager that asks with limit and ends at X-Total-Count.
The archived row's button says Sync like every other row, with a tooltip that automatic syncs are off for archived mirrors and the button syncs it now. Settings text, README and env docs point to Sync.
- Raise the zod floor to 4.6.7 (GHSA-mrhx-c7xc-m3w5). Astro's nested 4.5.4 resolves to the same copy and an override keeps both there. No other package in bun.lock changes.
- Add .github/dependabot.yml: bun for the root, npm for www, open-pull-requests-limit 0 so only security updates open pull requests.
- Record each mirror's destination size (Git plus LFS) and its release asset bytes and count during sync, from responses the sync already fetches, so no new requests. Four nullable columns on repositories, migration 0021.
- GET /api/storage and a Storage page with cards, bars by destination owner and for the largest repositories, and a sortable table with search, an owner filter and a release limit hint.
- Run sources.test.ts in an isolated child process so leaked @/lib/sources mocks cannot reach it.
Closes#479
Co-authored-by: Florian Zevedei <florian.dahn@fdsoftware.de>
- Read only API keys: an Access choice when creating a key, stored in
the key metadata; the middleware refuses writes and most auth calls
for them with 403 API_KEY_READ_ONLY.
- POST /api/mcp: stateless MCP endpoint with 8 tools that call the REST
API over loopback with the caller's key. Needs x-api-key, a cookie
alone gets 401. MCP_INTERNAL_URL overrides the loopback address.
- mcp/: gitea-mirror-mcp, a zero dependency stdio bridge to /api/mcp for
Claude Desktop and Cursor, with a CI workflow that publishes it on an
mcp-v* tag.
- e2e spec for keys, the endpoint and the bridge; docs for the endpoint,
the bridge and read only keys.
The Spline scene kept the GPU at about 45% on an M1 Pro for the whole
visit, even after scrolling past the hero, and phones downloaded and ran
it for a hidden canvas.
- Load Spline only at md and up, after the page loads, and not at all
with reduced motion
- Pause the scene and the shader while the hero is off screen
- Render Spline at 1.5x instead of the device ratio and the shader at
half resolution
- Size the static logo to match the 3D mascot so the swap does not jump
- Drop backdrop blurs on the feature cards and CTA that changed no
visible pixels
- Make the header scroll listener passive
Measured on the built site: GPU use went from about 44% to 28% at the
top and from 42% to 5% below the hero, and the phone page weight went
from 1.19 MB to 0.33 MB.
- JSON 404 for an /api path or method without an endpoint, instead of
the HTML page.
- The 409 from sync/repository and sync/organization carries the stored
record.
- role is optional on sync/organization (default member, force keeps the
stored role, unknown role is 400); the not-found message mentions user
accounts.
- GET /api/repositories/:id and GET /api/organizations/:id.
- API docs: organization endpoints, host and path for pasted URLs, and
the Content-Type header every write needs.
- astro 7.3.2 to 7.3.5 in the app and the site for GHSA-mpr7-6m5w-76rw,
with @astrojs/node 11.1.6.
- A vite ~8.1.6 override in the app: its bun.lock still had vite 8.0.0
and 8.1.5, inside the October vite advisories.
- git-lfs is built with Go 1.27.3, golang.org/x/net 0.61.0 and
golang.org/x/crypto 0.58.0.
- Raise the floors for sharp, smol-toml, source-map-js and
http-cache-semantics in the app and the site, and pin the site's vite to
8.1.6.
- The VEX product for the published image is now
pkg:docker/raylabshq/gitea-mirror?repository_url=ghcr.io. The old
pkg:docker/ghcr.io/... form never matched, so the zlib CVE-2026-85091
exception was ignored on main scans.
Adds a Pre-releases switch (stable only), include and exclude tag patterns (globs or /regex/) with a preview of what a save keeps or removes, per organization and per repository overrides, and RELEASE_TYPE, RELEASE_INCLUDE_TAGS and RELEASE_EXCLUDE_TAGS. Releases in the destination that stop matching are removed on the next sync, and a warning is raised when that empties a destination.
Adds notifyOnNewOwnRepo, notifyOnNewOrgRepo and notifyOnNewStarredRepo under the existing new repository switch. They default to on, so saved configs keep their current behavior without a migration.
Adds GitHub App installation as an authentication option for GitHub sources, with installation token refresh for Gitea 1.27+ pull mirrors, credential redaction, and the account filters applied to installation listings.
Tailwind v4 detects sources automatically and reads every file under the
project root that is not ignored by git. The app writes repository
backups to data/repo-backups by default, and that directory sits inside
the project root on source installs such as the Proxmox script. With
large git bundles there, the build read them into memory and the
process was OOM-killed at the server entrypoints step.
Limit the Tailwind source scan to src/, which is the only place the app
uses utility classes. This also stops the docs site under www/ from
contributing classes to the app stylesheet. Ignore data/ as a whole in
git, keeping its README.
Verified in an unprivileged LXC with a 2 GiB limit: with a 3 GB file
under data/repo-backups the build was killed, and with this change it
completes. Every class dropped from the stylesheet came from www/,
docs or workflow files, none from src/.
* Enable grouped new repo notifications
Add support for the "New repository discovered" notification, including UI/docs updates and tests. Discovery and scheduler imports now send one aggregated alert per owner or organization with capped repo lists, and notification config loading/decryption was refactored so background sends reuse the same safer logic.
* Group starred repository notifications
Send one notification for all newly imported starred repositories, regardless of owner. Generalize discovery notifications to describe imports grouped by owner or as a single starred group.
On Bun 1.4 the node adapter's reader cancel closes the stream before the
request abort fires, so the abort listener's controller.close() threw
ERR_INVALID_STATE as an uncaught exception and the process shut itself
down whenever a dashboard tab disconnected. Both SSE routes now guard the
close and clear their timers on cancel too.
A shutdown forced by an uncaught exception or unhandled rejection now
exits with status 1 instead of 0, so a unit with Restart=on-failure
(the Proxmox community script) starts the service again.
* fix: assign mirrored issues and pull requests to their milestones (#460)
Milestones were created in Gitea but issues never pointed at them: the
issue and PR payloads had no milestone field, and milestones were
mirrored after issues anyway.
Milestones now sync first and return a title to Gitea id map, which the
issues and pull requests passes use to set the milestone on create and
to set, change or clear it on update. The edit only carries a milestone
when it differs, so Gitea writes no repeat timeline entries. Unchanged
items are fixed on the weekly full pass from the listing alone. When
milestones are not mirrored, or one is missing in Gitea, the issue is
left as it is.
* fix: review follow-ups for milestone assignment (#460)
- Never clear a milestone made in Gitea alone. The milestones pass now
returns only the milestones that mirror a GitHub one, and an issue is
only cleared when it sits on one of those.
- Match existing pull request issues by a [PR #N] prefix, so a mirrored
issue that mentions [PR #N] in its title is not taken for that PR.
- The milestone edit on an unchanged pull request is best effort: a
failure is logged instead of retried, and the pass still completes.
- Record a milestone edit on the cached Gitea issue so a retry does not
send it again.
- Store 0 instead of undefined when a created milestone has no id.
* fix: keep mirrored milestones in line with GitHub (#460 follow-up)
Milestones were only ever created, so closing, reopening, moving the due
date or editing the description on GitHub never reached Gitea. The
milestones pass now edits an already mirrored milestone when its state,
due date or description differs.
Due dates compare as calendar days. Gitea moves an edited deadline to
the end of that day in its own time zone, so comparing instants would
edit the milestone on every sync. A removed due date is cleared with
Gitea's year 9999 value, since its edit endpoint ignores an empty one.
The test failed in CI because another file replaces bun:sqlite with
mock.module, which leaks across files depending on order. It now runs in
an isolated child process like the other fetch and sqlite driven suites.
Each case builds its database with manage-db init, so the tables come
from drizzle/ instead of hand written DDL that could drift, and the
bundle is built once in beforeAll instead of at import time.
The Dockerfile rewrite of the manage-db scripts is now a one line sed.
Mirror job timestamps are stored to the second. A sync that starts and
finishes in the same second writes its "syncing" and "synced" events
with the same timestamp, and SQLite returned the tie oldest first, so
the summary took the start event as the repository's current state.
The activities and dashboard queries break ties on rowid, newest first,
and the summary counter prefers a finish over a start from the same
second whatever order they arrive in.
The site moved to pnpm in July 2025 and pnpm-lock.yaml is the lockfile
kept up to date, but www/bun.lock was left behind and still pins Astro 5.
The Cloudflare Pages build image now installs with bun when it finds a
bun.lock, and bun install --frozen-lockfile stops on the stale file, so
every docs build has failed since.
Remove it, ignore it in www so a local bun install cannot bring it back,
and point the site README and CLAUDE.md at pnpm.
* fix: resume issue and PR passes after a rate limit, cheap full passes (#449)
A pass stopped by the rate limit now saves the numbers it finished with
the repository, and the next run repeats the same pass and skips them
unless they changed since it started. Before, every run started over, so
a repository too large for one hour of budget never completed a pass.
The weekly full pass no longer fetches comments, PR details, commits or
files for items already in the destination and unchanged since the last
completed pass. It corrects title, body, state and labels from the
listing alone.
A sync paused by the rate limit logs one line instead of the error
object with every partial result.
* chore: retrigger docs preview
The v3.37.0 change stopped the scheduler run from holding its lock
through a rate limit, but only the refused request stopped. Every other
call queued in the run still went to GitHub and came back 403, and the
retry helper sent each of them three more times. GitHub counts requests
made while limited toward abuse detection, and a reporter's account was
suspended that way.
Every GitHub client now carries a request wrap that reads the gate
before the network: a reset within two minutes is waited out inside the
request, a longer one fails at once with a RateLimitPausedError, and
only the rate limit probe endpoint is exempt. The retry helper does not
retry a rate limit refusal and stops starting the remaining items,
surfacing a RateLimitedBatchError. The metadata component catches in
both mirror paths rethrow rate limit errors so the repository fails and
the scheduler restores it, and the pull request detail fallback no
longer writes a stripped down issue after a refusal. The secondary rate
limit handler closes the gate for its retry-after window.
A cards/list switch in the toolbar. The list shows one row per
organization with its role, destination, repository counts, last
mirrored time, status, the same actions as the cards and the source
and destination links, so a few dozen organizations fit on one screen.
The choice is stored in localStorage next to the theme and time format
preferences.
The actions dropdown that existed twice in the card is now one shared
component used by the cards and the list, and the status badge, source
resolution and repository breakdown helpers moved to a shared module.
After a complete pass, the issues and pull request passes list only the
items GitHub reports as updated since the previous listing (minus a 10
minute margin), so per-issue comment listing and per-PR detail, commits
and files calls run only for what changed. Pull requests use the issues
endpoint for this since pulls.list has no since filter.
The watermark lives in the repository metadata state as syncCursors, one
per kind, and records when the listing started. It only moves when the
pass finishes with no failed item. A full pass still runs when the last
one is older than 7 days, when the destination has no mirrored issues or
pull requests, and after reset metadata, which clears the state.