* 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.
Fixes GHSA-p7w3-46pg-mv6h. The 3.38.1 guard covered the connection tests,
OIDC discovery and the notification providers, but a source URL stored
through POST /api/sources, PUT /api/sources/:id or the configuration save
was only checked for its scheme, and the source fetch helper called fetch
directly with the upstream body in the error message. The organization
sync route then forwarded that message when it matched a safe pattern.
- The three routes refuse link local and metadata addresses for source and
destination URLs with the same message the connection tests give.
- sourceFetch goes through safeFetch: refused addresses, plain http pinned
to the checked address, no redirects.
- SourceApiError messages carry the URL and status only; the body stays on
the error and is logged.
- createSecureErrorResponse never forwards SourceApiError or HttpError
messages, whatever words they contain.
* fix(security): guard connection tests, close sign-up and scope SSO providers
Fixes the four privately reported issues GHSA-9m33-xfrc-5jxw,
GHSA-6m23-28hh-gjh2, GHSA-2hpx-83vg-gm45 and GHSA-5pp8-r7f5-6q8p.
- The Gitea and GitHub test-connection routes require a signed-in user.
They were the only non-public API routes without a guard and made a
server side request to any URL in the body.
- Email sign-up is refused server side once an account exists unless
AUTH_ALLOW_SIGNUP=true. Only the signup page redirected before; the
endpoint accepted new accounts from anyone reaching the instance.
- SSO providers are scoped to their owner for list, update and delete,
and the client secret is never returned. An empty secret on update
keeps the stored one, and the form says so.
- HttpError messages carry the status only; the upstream body stays on
the response field for logs and never reaches API clients.
- A narrow outbound guard for user supplied URLs (connection tests, OIDC
discovery, ntfy, Gotify, Apprise, webhook): link local and cloud
metadata addresses are refused, host names are resolved and checked,
and redirects are not followed. Private networks stay allowed since
mirroring to a LAN Gitea is the normal deployment.
* fix(security): pin plain http requests to the checked address
The guard resolved a host name and checked its addresses, but fetch resolved
the name a second time, so a name that changes its answer between the two
lookups could still reach a link local address. Plain http requests now go
to the address the check saw, with the original name in the Host header.
https keeps the name: the certificate is validated against it and the
metadata services speak no TLS.
* feat(releases): mirror releases from Gitea and Forgejo sources (#440)
Release mirroring was GitHub only. The list-and-reconcile loop in gitea.ts
now works on a normalized release shape that a per-source lister fills, so
Gitea, Forgejo and Codeberg repositories mirror their releases and assets
the same way GitHub ones do.
- GiteaSourceProvider gains listReleases(owner, repo, limit), paginating
with limit/page and stopping at x-total-count. listReleases is optional
on the SourceProvider interface; GitLab does not implement it.
- mirrorGitHubReleasesToGitea keeps its signature and its behaviour, with
the "Originally published on GitHub:" header line unchanged byte for
byte so existing mirrored bodies are not PATCHed on the next sync.
Gitea and Forgejo releases get the same shape with their own label.
- mirrorGiteaSourceReleasesToGitea is the new entry point. It honours the
same release limit, asset limit, per-destination lock, tag-exists check,
retention pruning and asset reconciliation.
- reconcileReleaseAssets takes the download Authorization from the caller
instead of always sending the primary config's GitHub token: the GitHub
path sends its token, a Gitea source sends that instance's token, and a
tokenless public source sends no header at all. One host's token can no
longer reach another host.
- mirrorReleases left GITHUB_ONLY_METADATA_KEYS and is gated on the new
sourceKindSupportsReleases instead. Issues, pull requests, labels,
milestones and the metadata master switch stay GitHub only, and GitLab
still has releases forced off.
- The three call sites pick the path: a GitHub source with a client goes
through Octokit, a Gitea source through its own API, anything else logs
and is skipped.
- The release switch and its two limits follow the release-capable check
in the settings UI, and the docs, the site and the capability tables say
what is true now.
* test(releases): run the source aware release suite in an isolated child
Other test files replace the HTTP client and related modules with mock.module,
which is process-wide in bun and depends on file order. On CI the new suite
ran after one of them and every case failed on a mocked destination lookup.
Same harness as gitea-org-mirror-destination.test.ts.
The repositories table is unique on (user_id, source_id, normalized_full_name)
so the same owner/name can live on two hosts. Discovery used that key to
decide what was new, so two source rows on one host (an organization pinned
to a public-only github.com source next to a personal github.com token) each
inserted their own row for one repository, and the scheduler mirrored both to
the same destination in the same batch.
A repository is now identified by its host and full name whichever source
lists it: personal discovery, organization re-discovery and the two import
endpoints skip repositories the user already tracks on that host, and each
scheduler pass runs one row per upstream repository, logging the duplicate
rows it skips. Found while investigating #417.
Three things combined to make crash recovery restart live jobs:
- isJobInterrupted treated any job older than two hours as interrupted before
looking at its checkpoint, so a large job that heartbeated every two minutes
was resumed while it was still running. The age rule is gone: a checkpoint
is the job's proof of life, and only a missing or stale one (ten minutes)
marks a job interrupted.
- processWithRetry only checkpointed the item that tripped the interval, so a
resume re-processed up to N-1 finished items. Every completed item is now
recorded; the interval only throttles the published progress event.
- updateMirrorJobProgress was a read-modify-write of completedItemIds with no
lock, so two items finishing in the same tick could drop each other. Writes
are serialized per job with the keyed lock from #419.
A resumed job also records a fresh start time. Found while investigating #417.
Gitea and Forgejo accept any number of attachments with the same name:
CreateReleaseAttachment has no name-collision check, so every upload POST
appends another attachment. Asset reconciliation listed what a release
already had and then kept uploading for tens of seconds or minutes with no
mutual exclusion, so two passes over one repository in the same run both
read "missing" and both uploaded. That wide check-to-write window is what
left two, three or five copies of every asset (#417).
The classifier now groups existing attachments by name, keeps one copy and
reports the surplus and stale ones for deletion. Reconciliation fails
closed: nothing is uploaded when the attachment list cannot be read,
deletes run before any upload, and an asset whose stale copy could not be
deleted is skipped until the next sync. Release reconciliation holds a lock
keyed on the destination repository, so two passes cannot interleave even
when they come from two repository rows. The sync path and both mirror
paths claim the row with one conditional UPDATE instead of reading the
status and writing it in a second statement.
The organization destination editor treated a typed value equal to the
organization's own name as "reset to default" and saved null. That is the
default only under the preserve strategy. Under single-org the default is the
configured destination organization, so an override of "GrapheneOS" on the
organization GrapheneOS was dropped and every repository kept landing in the
destination organization. Three display lines hid it: the custom badge never
showed for such a value, the card read "GrapheneOS -> GrapheneOS" with no
override stored, and the placeholder, helper text and reset label all stated
the preserve default whatever the strategy.
Both destination editors now save what was typed and clear the override only
for empty input, through a shared resolveDestinationSaveValue helper. The
organization editor takes the default for the configured strategy as a prop
and uses it everywhere it used to name the organization, and any stored
destination counts as an override. Reset to Default no longer saves the old
value again, which it did because setEditValue does not reach the handler's
closure.
The repositories list response now carries the organization destination
overrides, so a repository row shows where its organization actually sends it
instead of the strategy's owner, which is why the reporter believed the
repository level was ignored too.
Server side is unchanged: getGiteaRepoOwnerAsync already resolved starred
mode, then the repository override, then the organization override, then the
strategy. Tests cover that order for single-org and the new save rule.
Fixes#416
Every one of these advisories was published on 2026-09-08 between 20:30 and 21:26 UTC, against the versions this repository was already pinning from the earlier rounds. Same package names, new CVEs, not a regression from anything that merged today.
**Application** (the nine Docker Scout alerts on main)
| Package | Was | Now | Advisory |
| --- | --- | --- | --- |
| astro | 7.1.0 | 7.3.2 (floor ^7.2.8) | GHSA-26w7-cxv4-gfx2, remote code execution through AVIF image optimization (critical) |
| @xmldom/xmldom | 0.8.13 | 0.8.15 | eight advisories, CVE-2026-83605 through CVE-2026-83619 (high) |
| sharp | floor ^0.35.0 | ^0.35.4 | GHSA-rgj7-g3m4-5g8c, libheif |
| js-yaml | floor ^4.3.1 | ^4.3.2 | GHSA-2883-xcg3-v3hh |
| svgo | (no floor) | ^4.1.0 | GHSA-w27v-7q3p-w38r, GHSA-4vpr-x523-8j87 |
**Documentation site** (the six Dependabot alerts against `www/pnpm-lock.yaml`)
astro to ^7.2.8 (resolves 7.3.2), and pnpm overrides for sharp ^0.35.4, svgo ^4.1.0 and js-yaml ^4.3.2. That covers all six, including the moderate Astro authorization bypass (GHSA-376h-93r7-7g6f) which 7.2.4 already fixed.
This supersedes the two Dependabot pull requests, which only cover the site and overlap with each other.
Checked: 906 tests pass, `bun run build` passes, and the site builds its 37 pages. The lockfile change is confined to the bumped packages and their transitive resolution.
Follow-up to #409. Public-only accounts could add and mirror an organization, then hit the source-token gates that were left behind everywhere after the first mirror. Each item was reproduced against a running instance with only a destination configured and a tokenless GitHub source, and re-checked after the fix.
**The paths that still demanded a source token**
- `POST /api/job/sync-repo` and `POST /api/job/schedule-sync-repo` answered 400 "Config missing for the user or token", so the Sync button never worked for these accounts. Neither route uses that token; they dispatch to the destination and resolve source clients per repository. Both now gate on the destination token, like the mirror and retry routes already do.
- `recovery.ts` threw "GitHub token not found in configuration" before the per-source resolution right below it, so an interrupted mirror never recovered. The gate is gone and a tokenless GitHub source now gets the same anonymous client the job routes build.
- `mirrorGitRepoIssuesToGitea` and the pull request, label and milestone functions required the config token even though the client arrives as a parameter, so with the anonymous client they threw and the per-component try/catch logged an error on every sync. They now require only the destination, and public metadata mirrors.
**The bulk import**
`POST /api/sync` iterated every source, including tokenless ones whose personal listing 401s, counted them as failed, and answered 502 when there was nothing else. It now skips them the way `importRepositoriesFromSources` does, reports them as `skippedPublicSources`, and says so when every source is public only.
**The organization pin**
`force: true` without a `sourceId` or `provider` re-pinned the organization to the primary source. Reproduced with two sources: an organization pinned to the GitLab source and one deliberately left on every source were both moved onto the primary. Only a request that names a source re-pins now; naming a provider (public mode) still re-pins as #409 intended. Covered by a new route test.
**The badge**
`invalidateConfigCache()` cleared the cache but nothing re-read it, so the Public only badge appeared only after a reload. Mounted `useConfigStatus` hooks now refresh on invalidation, through one shared in-flight request so a page with several of them still makes a single call.
Two things from the review I deliberately did not change: the anonymous 60 requests an hour budget with its long `onRateLimit` waits, and `rediscoverOrganizationRepositories` running for every user rather than only tokenless sources. Both are judgement calls about the feature rather than defects.
Tests: 906 pass. Build passes. Type errors in the touched files are unchanged from main.
Follow-up to #407.
The bulk import cleared an organization's pin whenever another connected source listed the same organization name. That also undid pins a user had set on purpose through the card picker or the add dialog: pin acme to GitHub because the GitLab group of the same name is unrelated, and the next Import widened it back to every source.
Now the import only clears pins it created itself in the same run. Organizations it inserts (or re-pins on recovery) are remembered by name for the rest of the run; when a later source lists one of those names, the pin is cleared, since the organization spans several sources, the same case migration 0020 leaves unpinned. Pins that existed before the run are never touched. The bookkeeping is applied after each source's transaction commits, so a source that fails part way leaves it unchanged.
- `selectSameRunPinsToClear` in `src/lib/sources.ts` holds the rule, with unit tests.
- `src/pages/api/sync/index.ts` tracks the run's pins and applies the rule; the existing-org lookup goes back to name to status.
- Changelog line under the per-organization source selection entry.
Tests: 878 pass. Build passes.
Organizations can pin the source they mirror from when more than one source is connected.
- New nullable organizations.source_id column (migration 0020_org_source), backfilled only when an organization's repositories agree on exactly one source.
- NULL means every source; a pin whose source was deleted falls back to the same. A valid pin scopes the org mirror job, the repository count breakdowns and org deletion to that source.
- POST /api/sync/organization persists the pin, PATCH /api/organizations/:id sets or clears it, the bulk import stamps discovered orgs and clears the pin when a second source lists the same org.
- UI, only with more than one source connected: a Source picker in the Add Organization dialog, a per-card inline source editor with an Every source option, and per-org View on source links.
- The org mirror repository query now filters by user.
- Shared provider icon map in src/lib/source-providers/icons.ts.
A user can connect several sources (GitHub, GitHub Enterprise, GitLab, Gitea/Forgejo) and every repository remembers which source it came from. New sources table (migration 0019) with repositories.source_id backfilled by provider and host, per-user unique indexes widened to include the source, a Sources card on the Configuration page with per-source lock confirmations, and credentials resolved from each repository's own source in discovery, import, scheduling, organization mirror, retry, recovery, push targets, cleanup and reconcile. The legacy githubConfig connection fields mirror the primary source so scripts and env setups keep working.
### Fixed
- Added missing release asset mirroring functionality (APK, ZIP, Binary files)
- Release assets (attachments) are now properly downloaded from GitHub and uploaded to Gitea
- Fixed missing metadata component configuration checks
### Added
- Full support for mirroring release assets/attachments
- Debug logging for metadata component configuration to help troubleshoot mirroring issues
- Download and upload progress logging for release assets
### Improved
- Enhanced release mirroring to include all associated binary files and attachments
- Better visibility into which metadata components are enabled/disabled
- More detailed logging during the release asset transfer process
Fixes#68
Co-Authored-By: Claude <noreply@anthropic.com>
This patch completes the authentication fixes from v3.2.4, specifically addressing the releases mirroring function that was missed in the previous update.
Fixes:
- Critical authentication error in releases mirroring (encrypted token usage)
- Missing repository existence verification for releases
- "user does not exist [uid: 0]" error for GitHub releases sync
Improvements:
- Duplicate release detection to prevent errors
- Better error handling with per-release fault tolerance
- Enhanced logging with [Releases] prefix for debugging
Issue: #68
Fixed critical authentication issue causing "user does not exist [uid: 0]" errors during metadata mirroring operations. This release addresses Issue #68 and ensures proper authentication validation before all Gitea operations.
Key improvements:
- Pre-flight authentication validation for all Gitea operations
- Consistent token decryption across all API calls
- Repository existence verification before metadata operations
- Graceful fallback to user account when org creation fails
- Enhanced error messages with specific troubleshooting guidance
- Added diagnostic test scripts for authentication validation
This patch ensures metadata mirroring (issues, PRs, labels, milestones) works reliably without authentication errors.
### Added
- Comprehensive mobile and responsive design support across the entire application
- New drawer UI component for enhanced mobile navigation
- Mobile-specific layouts for major components
- Mobile screenshots in documentation
### Improved
- Enhanced mobile user experience with optimized layouts
- Updated organization list cards with better mobile responsiveness
- Better touch interaction support throughout the application
### Fixed
- Type definition issues resolved
- Removed unnecessary console.log statements
### Documentation
- Updated README with mobile usage instructions and screenshots
- Added mobile-specific documentation sections
- Fix mixed mode organization strategy not persisting after page refresh
- Update README to be more concise and use RayLabsHQ organization links
- Remove unimplemented OIDC/SSO authentication references
- Update dependencies to latest versions
Major repository migration from arunavo4 to RayLabsHQ organization.
This release includes updated Docker image locations and all repository
references throughout the codebase.
- Updated package.json version from 2.13.0 to 2.13.1
- Added CHANGELOG.md entry for v2.13.1 with Docker workflow improvements
- Includes Docker Scout migration, CI/CD enhancements, and package updates
- Fixed SQLite 'no such table: mirror_jobs' error during application startup
- Implemented automatic database table creation during database initialization
- Resolved database schema inconsistencies between development and production environments
- Enhanced database initialization process with automatic table creation and indexing
- Added comprehensive error handling for database table creation
- Integrated database repair functionality into application startup for better reliability