Commit Graph
852 Commits
Author SHA1 Message Date
ARUNAVO RAY f581292912 fix(ci): scan the pushed tag instead of a digest in Docker Scout (#436)
The Scout action rejects a digest reference with "missing or wrong image
input", which failed the v3.36.2 tag build after the push and skipped the
Helm chart publish. Scan the tag the run pushed: the release tag on a tag
build, edge on a main or scheduled build.
v3.36.2
2026-09-16 07:53:56 +05:30
Arunavo Ray 8f60ccf6dc chore: bump version to 3.36.2 2026-09-16 07:39:36 +05:30
ARUNAVO RAY c8618743c8 fix(settings): show the schedule that actually runs on the automation card (#435)
A fresh install started with scheduling enabled on a plain 24 hour
interval counted from first login. The automation card only knows clock
schedules, so it showed the 22:00 placeholder as if it were saved while
the scheduler ran at whatever time the user first signed in. Toggling
the card off and on happened to store the clock schedule and hide the
mismatch (#427). The card also showed the browser timezone whenever UTC
was stored, so a stored 22:00 UTC read as 22:00 local.

New configs start with scheduling off unless SCHEDULE_ENABLED or an
interval variable turns it on, with the same rule as the environment
loader, and the built-in default is the 22:00 clock schedule the card
shows. When a plain interval is stored (SCHEDULE_INTERVAL,
GITEA_MIRROR_INTERVAL or an older version) the card says what runs and
how it got there, and leaves the frequency and start time unset until
the user picks one. The timezone chip shows the stored timezone and
turns into a one-click switch to the browser timezone when they differ.
2026-09-16 07:39:22 +05:30
ARUNAVO RAY 2ff2e71d18 fix(organizations): ignoring an organization stops its repositories from syncing (#433)
Ignore Organization only changed the organization row. Its repositories
kept their mirrored status, so the scheduler went on syncing them, and
rediscovery kept importing new repositories for the organization, which
auto-mirror could pick up (#429).

The status route now takes a cascade flag, which the UI always sends.
Ignoring an organization ignores its idle repositories; rows in flight or
being deleted finish on their own and rows already ignored stay that way.
Including it again restores the ones it touched: mirrored when the
repository has been mirrored before, imported otherwise. Rediscovery skips
ignored organizations. The route is covered by a test against the real
schema in an in-memory database.
2026-09-16 07:39:18 +05:30
ARUNAVO RAY 2d1795051d docs(nix): import the module before enabling services.gitea-mirror (#434)
The README, NIX.md and the production snippet in the deployment guide set
services.gitea-mirror straight after adding the flake input, but the
option only exists once gitea-mirror.nixosModules.default is imported.
Only the quick start snippet showed that (#426). Every snippet that
enables the service now shows the import and where the input goes.
2026-09-16 07:31:52 +05:30
ARUNAVO RAY 9c11d398e9 fix(ui): name the configured destination on organization cards (#432)
The organization cards hardcoded the Gitea icon, the Gitea button label and
the View on Gitea tooltip, while the repository table already named the
configured destination. A Forgejo user saw Gitea on every card (#430).

Both card layouts now use the destination icon and label, and the tooltip
text follows the destination too.
2026-09-16 07:31:48 +05:30
ARUNAVO RAY 3df3fa577e fix(ci): stop main builds from overwriting the latest image tag (#431)
A merge and the release bump that follows it land on main seconds apart.
Both triggered the Docker workflow and both pushed latest, and on v3.36.1
the older build finished last, so latest carried 3.36.0 until the weekly
rebuild replaced it (#425).

The workflow now runs one build per ref at a time, cancelling superseded
branch builds but never a tag build. The latest tag is only pushed by a
stable release tag build, main builds push edge and the short sha, and
Docker Scout scans the digest the run just pushed instead of whatever
latest pointed at.
2026-09-16 07:31:45 +05:30
Arunavo Ray 0759db80d3 chore: bump version to 3.36.1 v3.36.1 2026-09-11 05:44:49 +05:30
ARUNAVO RAY 0299be92b1 fix(discovery): track one row per upstream repository across sources on the same host (#424)
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.
2026-09-11 05:44:45 +05:30
ARUNAVO RAY 22a0aafcc1 fix(recovery): stop resuming jobs that are still running and checkpoint every item (#423)
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.
2026-09-11 05:35:03 +05:30
ARUNAVO RAY 3b78f0a8d3 fix(security): raise the smol-toml floor to 1.7.1 in the app and the site (#422)
GHSA-7w5x-hrqm-74c2: smol-toml 1.7.0 and below can be made to hang on a
malformed TOML document. Dependabot only flags the site lockfile (1.7.0);
the app lockfile had 1.6.0 through astro and is raised the same way.
2026-09-11 05:34:59 +05:30
Arunavo Ray 48e0c5c346 chore: bump version to 3.36.0 v3.36.0 2026-09-10 23:34:15 +05:30
ARUNAVO RAY 01f926d214 fix(deps): update @astrojs/node to 11.1.5 so the standalone server starts on astro 7.3 (#421)
Astro 7.3 moved the startup logger from app.pipeline.getLogger() to
app.getLogger(). The 3.35.1 security bump raised astro to 7.3.2 but left the
node adapter at 11.0.2, which still reads app.pipeline, so running
dist/server/entry.mjs directly crashes with "undefined is not an object
(evaluating 'app.pipeline.getLogger')" on every Bun version. Docker is not
affected because scripts/runtime-server.ts disables the adapter autostart;
the Nix package and bare metal systemd installs start entry.mjs and are.

Fixes #420
2026-09-10 23:34:02 +05:30
ARUNAVO RAY 1cc7a9a05c fix(releases): never duplicate release assets on the destination (#419)
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.
2026-09-10 23:33:58 +05:30
ARUNAVO RAY be0594b28f fix(orgs): keep a destination override that matches the organization name (#418)
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
2026-09-10 23:11:55 +05:30
Arunavo Ray c51fa958ba chore: bump version to 3.35.1 v3.35.1 2026-09-09 11:45:49 +05:30
ARUNAVO RAY e49cf0e367 fix(security): raise dependency floors for the 2026-09-08 advisories (#414)
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.
2026-09-09 11:45:32 +05:30
Arunavo Ray 199879a64c chore: bump version to 3.35.0 v3.35.0 2026-09-09 11:25:05 +05:30
ARUNAVO RAY 6b8d0b3d78 fix(public-orgs): finish the destination-only paths for tokenless sources (#411)
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.
2026-09-09 11:24:50 +05:30
ARUNAVO RAY 72a3223c76 fix(orgs): keep organization cards at column width when few are shown (#410)
The organizations grid used `repeat(auto-fit, minmax(27rem, 1fr))`. `auto-fit` collapses empty tracks, so with a single organization the one card stretched across the whole content area, while two or more sat at half width. `auto-fill` keeps the tracks, so a card is the same width no matter how many there are.

Both the list and the loading skeleton use the same class, so both changed.

Checked in the browser at 820, 1440 and 2000 px wide with one and with four organizations: one card now stays at column width, two columns at 1440, three at 2000, and a single column below the `sm` breakpoint, all unchanged from before for two or more cards.
2026-09-09 11:16:17 +05:30
TomRoyls 2c394bb722 feat: mirror public organizations without a source (#409)
Public organizations can be mirrored from GitHub, GitLab and Gitea/Forgejo without configuring a source.

- The Add Organization dialog offers a Public only mode (the default when no source is connected): pick a provider, optionally an instance URL, and the organization is imported anonymously.
- A tokenless source row is found or created for that provider and host, and the organization is pinned to it, so attribution, locks, the scheduler and cleanup keep working per source.
- New createPublicGitHubClient: a throttled anonymous Octokit for tokenless GitHub sources, used by the org mirror job, per-repo mirror and retry, the sync metadata client and the source provider. An empty token never reaches createGitHubClient.
- GitHub organization listings degrade to public repositories when the source is tokenless or the token cannot see the private and member listings.
- The scheduler gates on the destination token only, skips personal discovery for tokenless sources, and re-lists pinned organizations each tick so newly published upstream repositories appear.
- The Sources card can save a row with no username and no token and marks it Public only; the organizations and repositories pages work with no configured source.

Thanks to @TomRoyls. (#409)
2026-09-09 11:09:59 +05:30
Arunavo Ray e82aa86723 chore: bump version to 3.34.0 v3.34.0 2026-09-08 19:26:17 +05:30
ARUNAVO RAY c2ea2b3cb6 fix(orgs): bulk import only clears source pins it set in the same run (#408)
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.
2026-09-08 19:26:03 +05:30
TomRoyls a74b7e54ed feat(orgs): per-organization source selection (#407)
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.
2026-09-08 19:13:32 +05:30
Arunavo Ray 3a2f8b34ab chore: bump version to 3.33.0 v3.33.0 2026-09-07 06:49:25 +05:30
ARUNAVO RAY 7d17194e51 fix: review follow-ups for the organizations dialog and multi-source gating (#406)
Review follow-ups after #403 and #404: dialog copy and placeholder, forge-aware owner parser, GitHub only mirror switches available while any connected source is GitHub, configuration header copy, self-contained encryption test. Also builds the Sources card token settings link from validated URL parts (CodeQL alert) and adds a VEX statement for CVE-2026-85091 in zlib, which nothing in the image can reach.
2026-09-07 06:49:21 +05:30
TomRoyls 7dfb867202 feat: multiple sources per user with per-repository attribution (#404)
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.
2026-09-07 06:25:27 +05:30
TomRoyls 4478c32db1 feat(ui): organizations page follows the configured source (#403)
The organizations page and the add dialog now follow the configured source: provider label, instance URL, icon and the organization noun (group on GitLab). The billing manager role only appears for GitHub. Adding an organization whose name contains a slash returns 400 with a hint to use the top level group. New tests cover getOrganization and listOrganizationRepositories on the GitLab and Gitea adapters.
2026-09-07 06:23:03 +05:30
TomRoyls 34babd4d8c feat(orgs): per-organization skip forks mirror option (#405)
Adds a Skip forks tri-state to the organization Mirror options dialog. An organization can opt out of importing and mirroring forks without touching the global switch, and the pin is honored by the bulk organization mirror, the add-organization route, bulk import and the scheduler.
2026-09-07 06:22:30 +05:30
Arunavo Ray 67ca35d893 chore: bump version to 3.32.1 v3.32.1 2026-09-04 07:53:51 +05:30
ARUNAVO RAY 4a40497ff2 fix(security): clear the util-linux and git-lfs scan alerts (#402)
Strip setuid bits and add an OpenVEX exception for the three util-linux CVEs Debian will not fix in trixie, pin golang.org/x/crypto 0.56.0 and x/net 0.58.0 in the git-lfs build, and build git-lfs with Go 1.27.1.
2026-09-04 07:53:47 +05:30
Arunavo Ray 9a5bc450c2 chore: bump version to 3.32.0 v3.32.0 2026-09-03 18:33:55 +05:30
ARUNAVO RAY ec655b12ce feat: move mirrors to another owner from the app and follow ones moved in Gitea (#400) (#401)
Changing a repository's or an organization's destination used to change a
label and nothing else: the mirror stayed where it was, sync failed once
someone transferred it in Gitea, and Retry created a second copy.

Sync now looks for a mirror of the same source anywhere on the destination
before failing, and the mirror path does the same before creating a copy.
Reconcile reports moved mirrors and can record their new location.

The destination editors offer to transfer the existing mirror on Gitea or
Forgejo, with a confirmation for organizations. Pending transfers, when the
token cannot create repositories under the new owner, are reported, and
sync follows the mirror once accepted. Nothing is deleted on either side.
2026-09-03 18:33:33 +05:30
ARUNAVO RAY c14473e3f8 docs(www): six new articles for sources, push destinations, API, reconcile and release assets (#389)
New use-case articles: mirror GitLab to Gitea, back up Codeberg and Forgejo repositories, mirror to GitHub or GitLab, automate mirrors with the API, adopt existing Gitea mirrors, and control release asset storage. Existing articles and the comparison page updated for the OCI Helm chart, the extra sources and destinations, and reconcile.
2026-09-02 21:11:55 +05:30
ARUNAVO RAY f7d830ac03 feat(www): homepage reflects sources, push destinations, API keys and the Helm chart (#388)
Hero copy mentions the GitLab, Gitea and Forgejo sources and the GitHub, GitLab and Forgejo destinations as beta with GitHub to Gitea as the supported default. Nine feature tiles with beta pills for sources and push destinations, five new FAQ entries linking into the docs, host neutral wording in the existing answers, footer links to the new docs pages, and the structured data version brought up to 3.31.0.
2026-09-02 21:10:03 +05:30
ARUNAVO RAY 68b493292e docs(www): sources, destinations, API and reconcile pages for 3.31 (#387)
Four new documentation pages (sources, destinations, API and API keys, cleanup and reconcile), updates to the configuration, environment variables, quickstart, architecture and advanced pages, refreshed configuration screenshots and new ones for the destination dropdown and the API keys card. Also fixes the path prefix variable name in docs/API.md and the stale follow-up note in docs/SOURCE_PROVIDERS.md.
2026-09-02 21:04:49 +05:30
Arunavo Ray 0be31d8c58 chore: bump version to 3.31.0 v3.31.0 2026-09-02 19:10:22 +05:30
ARUNAVO RAY e4cf48e4a1 chore(config): GitHub to Gitea is the default, every other host is beta (#386)
The GitLab and Gitea / Forgejo sources and the Forgejo, GitHub and GitLab destinations show a beta pill and beta wording; the docs say GitHub to Gitea is the supported path. Testing every source and destination pair against real hosts turned up three bugs, fixed here: the pull mirror path never recorded a row's destination host, reconcile counted rows mirrored to a previous destination as missing, and the first push into a freshly created GitLab project failed because the host reports it as not found for a couple of seconds.
2026-09-02 19:09:59 +05:30
ARUNAVO RAY 30110a7745 fix(deps): merge the duplicate overrides blocks in package.json (#385)
The second overrides key added in #381 shadowed the original block, so JSON parsing kept only the four newer pins and the seven older ones were silently ignored on any re-resolve. One block with all eleven now; the lockfile records them and the resolved version set is unchanged.
2026-09-02 18:25:33 +05:30
Arunavo Ray ff734d48ab docs: forbid AI session links in commits and pull requests 2026-09-02 18:24:07 +05:30
ARUNAVO RAY 38fac54b17 feat(mirror): push engine for GitHub and GitLab destinations (#384)
GitHub and GitLab as mirror destinations, both beta. Neither has a usable pull mirror API, so the app keeps a bare clone of each source repository and pushes its branches and tags to the target with force and prune. Target adapters create, archive and delete repositories through each host's API. A dispatcher routes mirror, sync, retry, scheduler, recovery and cleanup by destination; Gitea and Forgejo are unchanged. Migration 0018 records each repository's destination host. Metadata, LFS, wiki and force push protection are disabled with a reason for push targets.

Closes #377
2026-09-02 18:23:27 +05:30
ARUNAVO RAY 8f9fc55c8e feat(helm): publish the chart to ghcr.io and keep its version in step with releases (#383)
The chart pinned appVersion 3.24.0 and nothing bumped it on release, so a default install ran an image six minor versions old, and the docs example pointed at a tag without the v prefix that does not exist on the registry.

The chart version now equals the application version. The tag build's sync job updates Chart.yaml alongside package.json, and a new job in the tag build packages the chart after the image is pushed and publishes it to oci://ghcr.io/raylabshq/charts/gitea-mirror. Docs and the website install tab show the OCI install.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 18:08:30 +05:30
ARUNAVO RAY 1e43dc4652 Update star history chart links in README 2026-09-02 17:00:01 +05:30
ARUNAVO RAY 83b59874a8 feat(cleanup): reconcile the destination with the repository database (#382)
Compare what Gitea or Forgejo holds under every owner this account
mirrors into with the repositories table. Mirrors of the configured
source that have no row are reported as untracked and can be adopted;
rows marked mirrored whose repository is gone are confirmed with a
direct check and can be reset to imported so the next run recreates
them. Native repositories and mirrors of other hosts are listed and
never touched, and nothing is deleted or archived. Exposed as
POST /api/cleanup/reconcile and as a dialog on the Automation tab.

Closes #284

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 16:16:57 +05:30
ARUNAVO RAY a1ff0433df fix(security): clear the image scan alerts (#381)
Build git-lfs with Go 1.25.14, stop serving the runner stage from the
layer cache so Debian security updates land, move typescript and
@astrojs/check out of the shipped dependencies, bump the nested copies
of browserslist, js-yaml, nanoid and sharp to patched versions, and
update browserslist in the website lockfile.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 16:16:46 +05:30
Arunavo Ray 26f78e06b8 chore: bump version to 3.30.0
Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
v3.30.0
2026-09-02 14:59:27 +05:30
ARUNAVO RAY 83dd74dd52 feat(auth): API keys for programmatic access (#380)
* feat(auth): API keys for programmatic access

Adds the @better-auth/api-key plugin so scripts and CI pipelines can call
the existing endpoints with an x-api-key header instead of a session
cookie. Keys are owned by a user, hashed at rest, prefixed gm_, never
expire unless an expiry is chosen, and are not rate limited. A small
guard plugin refuses key management calls that arrive with a key, so a
leaked key cannot mint or revoke keys.

New API Keys section on the Authentication tab with create, show once,
copy and revoke. Migration 0017 adds the api_keys table with the
validator fixture. docs/API.md documents the header and the calls
automation needs. An e2e spec covers create, use, refuse and revoke over
HTTP. bun.nix regenerated for the new package.

Closes #314

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX

* fix(e2e): send Origin on cookie-authenticated key management calls

Better Auth rejects a cookie-authenticated POST without an Origin header (403 MISSING_OR_NULL_ORIGIN). Browsers always send one, the Playwright request context does not, so the spec sets it on the create and delete calls. Also asserts the guard's 403 code and documents the Origin requirement for scripts that manage keys with a session.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 14:57:58 +05:30
ARUNAVO RAY 8d22774b0a feat(releases): limit release assets to the newest N releases (#379)
Adds a release asset limit next to the release limit. The release limit
still decides how many of the newest releases exist in Gitea at all. The
new asset limit decides how many of those also get their assets uploaded;
older releases inside the release limit are created with their notes and
tag only. Unset means assets for every mirrored release, which is what
happened before, so existing setups do not change. Zero means release
notes only. The limit inherits global to organization to repository
through the existing limit-key machinery, and lowering it never deletes
assets that were already uploaded; it only gates what a sync uploads.

Settings gets a second box next to the release limit, the overrides dialog
gets a matching row for organizations and repositories, and the env loader
reads RELEASE_ASSET_LIMIT. The README explains both limits and gains a
troubleshooting entry for the read-only push error people hit on pull
mirrors.

Closes #311
Closes #59

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 14:40:59 +05:30
ARUNAVO RAY 3ff05ec7f3 chore(deps): upgrade better-auth family to 1.7.2 (#378)
better-auth, @better-auth/oauth-provider and @better-auth/sso move from
1.6.23 to 1.7.2. This is the first stable line with the oauth-provider fix
for unbound resource indicators (Dependabot alert #55); no 1.6.x release
has it.

1.7 keys accounts by (issuer, accountId) and looks the credential account
up with issuer "local:credential" on sign-in, so migration 0016 adds the
column and backfills existing rows: local accounts get local:credential,
SSO accounts take their provider's issuer, other OAuth accounts get the
local:oauth namespace. It also adds jwks.alg and jwks.crv, the 1.7
oauth-provider columns, and the oauth_resources, oauth_client_resources
and oauth_client_assertions tables.

A database that booted the reverted 1.7.0-rc.4 build (on main for a few
minutes on 2026-08-06, and any dev database from then) already has part of
that schema without a 0016 record, which makes 0016 fail on every boot.
repairStrandedBetterAuth17Schema drops the leftovers before migrate runs,
in the same way the #312 repair handles stranded SSO columns; the
migration validator exercises it.

createOAuthClient no longer takes "type"; the SSO applications route sends
application_type (web or native) and reads it back with the legacy column
as fallback. bun.nix is regenerated. reset-users writes the issuer.

Supersedes #374.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 14:07:08 +05:30
ARUNAVO RAY d211e54877 feat: mirror from GitLab and Gitea/Forgejo sources (#376)
Adds a Source dropdown to the configuration card (GitHub, GitLab beta,
Gitea/Forgejo) backed by a source provider interface with three adapters.
Discovery, mirroring, recovery and cleanup go through the provider;
repositories record their source (migration 0015); clone credentials are
built per host and a repository from another host is refused at the
mirror sites. Issues, pull requests, releases and star lists stay GitHub
only. The destination card gets a Gitea/Forgejo dropdown, and both hosts
lock once repositories exist, with an explicit confirmation to change.

Closes #375. Helps #371.

Claude-Session: https://claude.ai/code/session_01Tp9pmi65a8k5jLMQFLf4JX
2026-09-02 13:42:34 +05:30