Commit Graph

731 Commits

Author SHA1 Message Date
ARUNAVO RAY 6ebf1916e8 chore(deps): update dependencies across app and website (#325)
* chore(deps): update app dependencies to latest in-range versions

* chore(deps): update website (www) dependencies to latest in-range versions
2026-06-14 12:25:45 +05:30
Arunavo Ray 91de0d1030 chore: bump version to 3.19.1 v3.19.1 2026-06-14 12:07:48 +05:30
Brendan Davidson 85bd1f4042 Repository table bulk actions (#322)
* Handle indexing when shift + clicking in the repository table

* Move the buttons when selecting rows

* Add in a bulk delete func in the repositories table

* Add bulk delete handler

* Make the single action use the bulk delete

* Delete the single repository id handler
2026-06-14 10:14:51 +05:30
Brendan Davidson 4a28015685 Skip the user defined orgs to ignore (#323) 2026-06-14 10:14:48 +05:30
Arunavo Ray da23941369 chore: bump version to 3.19.0 v3.19.0 2026-06-13 09:14:33 +05:30
Brendan Davidson 906ce57e8c Handle indexing when shift + clicking in the repository table (#316) 2026-06-13 09:14:02 +05:30
Arunavo Ray 1b84c75a97 chore: bump version to 3.18.0 v3.18.0 2026-06-13 08:17:20 +05:30
ARUNAVO RAY 0b6b6b76bf feat(github): add skipPersonalRepos toggle to mirror only org repos (#304) (#320)
- Add `skipPersonalRepos: z.boolean().default(false)` to githubConfigSchema
- Filter out user-owned repos in getGithubRepositories when flag is true
- Wire ONLY_MIRROR_ORGS env var to skipPersonalRepos in env-config-loader
- Add checkbox UI in GitHubMirrorSettings Filtering & Behavior section
- Round-trip skipPersonalRepos through config-mapper (UI ↔ DB)
- Add skipPersonalRepos to AdvancedOptions TypeScript type
- Mark include/exclude arrays in configSchema as unused/reserved
- Update ENVIRONMENT_VARIABLES.md to document ONLY_MIRROR_ORGS effect
2026-06-13 08:00:50 +05:30
ARUNAVO RAY 7610a614da fix: scheduler auto-start gate, backup clone URL, cancel-pending action, actionable 405 (#319)
* fix(scheduler): make enabled flag authoritative for auto-start

checkAutoStartConfiguration() and performInitialAutoStart() previously
used `scheduleEnabled || hasMirrorInterval`, allowing a configured
GITEA_MIRROR_INTERVAL to trigger boot-time auto-start even after the
user disabled scheduling via the UI toggle.

env-config-loader already writes scheduleConfig.enabled=true when
GITEA_MIRROR_INTERVAL is set at container startup, so the interval is
a timing detail, not an enable signal. The documented env-var contract
is preserved: GITEA_MIRROR_INTERVAL at boot → env-config-loader sets
enabled=true → auto-start fires. But a later UI disable now sticks.

Add a focused unit test for the gate logic.

* fix(backup): always derive clone URL from user-configured Gitea URL

The pre-sync backup preferred repoInfo.clone_url, which reflects
Gitea's ROOT_URL setting. In Tailscale MagicDNS deployments (and any
setup where ROOT_URL is an external address), this URL is unreachable
from the app itself, causing bundle backup to fail.

Always build the clone URL as:
  ${config.giteaConfig.url.trimEnd('/')}/${owner}/${repo}.git

This matches the URL the app already uses for all other Gitea API
calls and is guaranteed reachable.

* feat(jobs): cancel-pending endpoint + fix misleading Delete All copy

Add POST /api/job/cancel-pending that sets the current user's
repositories with status "imported" or "failed" to "ignored",
preventing the scheduler from re-queuing them. In-flight "mirroring"
rows are left alone. Returns the count and logs one activity entry.

Fix the "Delete All Activities" dialog to clearly state it only clears
the history log and does not stop pending work. Rename button/title to
"Clear History" so intent is unambiguous.

Add a "Stop Pending Mirrors" button (StopCircle icon, amber) in both
mobile and desktop activity log toolbars, with a confirmation dialog
explaining repos are set to Ignored and can be re-enabled from the
Repositories page.

* fix(sync): actionable 405 error for non-pull-mirror repos

Gitea returns HTTP 405 with an empty body when the target repository is
no longer a pull mirror — e.g. the mirror was auto-disabled by Gitea or
the repository lost its mirror state after a manual edit.

Previously this fell through to the generic error handler which stored
the raw HttpError message (often empty) giving the user no guidance.

Now a 405 response is caught alongside the existing 400 handler and
sets the repository to "failed" with an actionable error message:

  "Gitea reports this repository is not a pull mirror (HTTP 405).
  In Gitea check Settings → Mirror Settings; if the mirror section is
  missing, delete the repository in Gitea and re-mirror it from
  gitea-mirror."

The same message is written to the activity log for visibility in the
dashboard.
2026-06-13 08:00:47 +05:30
ARUNAVO RAY c28dcc209f fix(releases): stop delete/recreate cycle on permanent order mismatch (#310) (#318)
Root cause (Theory A): the `needsRecreation` check compared GitHub
published_at-based expected indices against Gitea's API order. Gitea mirror
repos sort releases by tag-commit date, which can permanently disagree with
published_at order (e.g. unaconfig_dart v0.1.0 published after v0.1.1 but
tagged before). This made `currentExpectedIdx < nextExpectedIdx` evaluate
true on every sync, triggering delete-all-and-recreate forever — spamming
Gitea's activity feed with "released X" events (#310).

Fix: replace the destructive order-check machinery with set-based
reconciliation via `classifyReleasesForReconciliation`. Releases are
created when missing in Gitea and skipped (or PATCH-updated if content
drifted) when already present. No deletions are ever triggered by ordering.
Retain the existing release-limit trimming (retention cleanup) unchanged.

Also removes the 1-second per-release delay that was only needed for the
creation-order dance, significantly speeding up initial mirrors.

Adds unit tests covering: normal ordered repos, the unaconfig_dart inversion
fixture, missing→create, present→skip, and edge cases.
2026-06-13 08:00:44 +05:30
ARUNAVO RAY 40ee3cbc44 fix(mirror): reuse existing same-source mirrors instead of creating suffixed duplicates (#315) (#317)
Starred (and other) repos duplicated on every re-mirror (starred/Repo,
Repo-owner, Repo-owner-1, ...) because the existence check only asked
"does a repo with this name exist?" and never "is the existing repo a
mirror of THIS same source?". The repo's own prior mirror counted as a
collision, so generateUniqueRepoName bumped to the next suffix each run,
repointing mirroredLocation at the newest copy. Under a single re-call,
3 concurrent/retried jobs each computed a DIFFERENT suffixed name, so the
location-based in-flight guard never matched and the race produced extra
copies.

Fix (source-identity aware):
- New shared helper src/lib/utils/mirror-source-match.ts:
  - normalizeCloneUrl / cloneUrlsMatch: credential-, .git-, slash- and
    host-case-insensitive clone URL comparison.
  - isMirrorOfSource: a Gitea repo is "ours" only if it is a mirror AND
    its original_url matches this repo's source.
  - findExistingMirror: resolves an existing same-source mirror via the
    recorded mirroredLocation first (survives strategy changes — #309),
    then the base candidate name.
  - classifyCandidateName: pure available/reusable/taken decision.
- gitea-enhanced: export GiteaRepoInfo and add original_url (Gitea's
  recorded migration source) for source matching.
- Both create paths (mirrorGithubRepoToGitea, mirrorGitHubRepoToGiteaOrg):
  run findExistingMirror BEFORE name generation; on a hit, reuse that
  location and route into the existing "already mirrored" handling rather
  than calling generateUniqueRepoName. Names now converge under
  concurrency so the in-flight guard becomes effective.
- generateUniqueRepoName is now source-aware: an occupied name held by a
  mirror of the SAME source is reused (no suffix); suffixing only happens
  on a genuine different-source collision, preserving #95/#236 behavior.
  The per-user DB claim check is retained so two users mirroring the same
  source into a shared org stay separated.
- Phantom-fork guard (#309): the existingRepoInfo.mirror branches now
  verify same-source before marking "mirrored"; on mismatch they fall
  through to unique-name generation and create a separate mirror.
- Scheduler: a `failed` repo whose mirroredLocation still resolves to a
  live same-source mirror is routed to syncGiteaRepo instead of re-create,
  breaking the failed-metadata re-create loop cheaply.
- Remove dead src/lib/starred-repos-handler.ts (zero importers across all
  git history); its correct base-name/.mirror reuse logic now lives in the
  shared helper.

Tests: src/lib/utils/mirror-source-match.test.ts (30 cases) covers URL
normalization, reuse at base name, reuse via mirroredLocation across a
strategy change, genuine different-source collision (suffix), phantom
fork, stale mirroredLocation fallback, per-user DB-claim separation, and
the suffix-vs-reuse classification. Full suite: 319 pass, 0 fail.
2026-06-13 08:00:41 +05:30
Arunavo Ray 699a5771f5 chore: bump version to 3.17.1 v3.17.1 2026-06-05 18:42:11 +05:30
ARUNAVO RAY e862714d6a fix(db): self-heal sso_providers duplicate-column crash on upgrade (#312) (#313)
Migration 0013 runs as a single transaction that rebuilds `organizations`
and then `ALTER TABLE sso_providers ADD saml_config` / `ADD domain_verified`.
On instances where those columns were already created outside Drizzle (declared
in schema.ts and added via db:push / an SSO-register round-trip on an
intermediate build), the ADD throws "duplicate column name: saml_config". That
rolls back the entire 0013 transaction, so 0013 is never recorded in
`__drizzle_migrations` and is retried — failing identically — on every boot,
crash-looping the server.

Add a pre-migrate repair (mirroring the existing repairFailedMigrations() for
the 0009 case): when 0013 is unrecorded but the columns already exist, preserve
any real SAML provider config, drop the stranded columns so the canonical 0013
runs in full (organizations rebuild included), then restore the preserved
values once the columns are re-added. No-op on fresh installs, clean upgrades,
and already-migrated databases.

This lets affected instances recover automatically on the next boot after
upgrading — no manual SQLite surgery required.

- src/lib/db/migration-repairs.ts: repairDuplicateSsoColumns + restoreSsoDataAfter0013
- src/lib/db/index.ts: wire both around migrate()
- scripts/validate-migrations.ts: cover the broken-upgrade + data-preservation path
2026-06-05 18:41:46 +05:30
Arunavo Ray 716981aa04 chore: bump version to 3.17.0 v3.17.0 2026-06-02 11:41:24 +05:30
ARUNAVO RAY 66e3284898 fix(sso): repair SSO login bounce + migrate to @better-auth/oauth-provider (#307)
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.
2026-06-02 11:40:54 +05:30
IYUANWEIZE 695f0ff005 Fix Docker image line in .env.example (#305) 2026-05-28 13:49:32 +05:30
Arunavo Ray 53f2cf36fc chore: bump version to 3.16.3 v3.16.3 2026-05-27 14:53:34 +05:30
ARUNAVO RAY 8ffcf3bdc6 fix: bridge header auth into a real Better Auth session (#303)
Header / forward authentication has been end-to-end broken since the
v3 rewrite. The middleware populated `context.locals.user` from
trusted upstream headers (Authentik / Authelia / oauth2-proxy /
Caddy), but never minted a Better Auth session, and never set a
cookie. Server-rendered pages saw the user, but the React SPA's
`/api/auth/get-session` call hit Better Auth's handler — which only
reads its session cookie — and got `null`. The auth guard then
redirected to `/login`, even though the upstream proxy had already
authenticated the user.

Reported on issue #29 by @lanrat with a clean repro on v3.16.1.

Fix: add a small Better Auth plugin (`header-auth`) that exposes
`POST /api/auth/sign-in/header`. The endpoint validates the trusted
headers via `authenticateWithHeaders`, creates a real session row via
`internalAdapter.createSession`, and attaches the `Set-Cookie` via
`setSessionCookie` — the same pattern the magic-link, anonymous, and
phone-number plugins use after their respective verification steps.

The Astro middleware now calls this endpoint when no cookie session
exists and header auth is enabled, forwards the `Set-Cookie` onto the
outbound response, and populates `context.locals` from the minted
session. After the first request the browser has the cookie; every
subsequent request takes the normal cookie-auth fast path and the
bridge doesn't fire.

Fail-open everywhere: any endpoint failure (header auth disabled,
auth rejected, DB blip, malformed response) returns null from the
bridge and the request proceeds as anonymous. A broken header-auth
configuration must never lock everyone out of the cookie-auth path.

Tests:
- `auth-header.test.ts` — unit tests for `extractUserFromHeaders` and
  `isHeaderAuthEnabled`, including lanrat's reported config shape
  (same header for username and email).
- `auth-header-plugin.test.ts` — locks down plugin id, endpoint key,
  path, and method so an accidental rename can't silently break the
  middleware bridge.
- `auth-header-bridge.test.ts` — covers the cookie-extraction logic
  and the fail-open paths (non-2xx, thrown error, malformed JSON,
  missing fields, no Set-Cookie attached).

Stacks on top of #301 (better-auth 1.6.11 bump).

Refs: #29
2026-05-27 14:53:16 +05:30
ARUNAVO RAY a07af96f84 chore: bump better-auth to 1.6.11 (#301)
Updates `better-auth` and `@better-auth/sso` from 1.5.5 to 1.6.11 to
pick up the patch fixes that have landed since (OAuth state CSRF
verification, scrypt non-blocking password hashing, account cookie
comparison fix, session freshness alignment, etc.). All 270 local
tests pass against the new version.

The only behavioral surface to watch:
  - `freshAge` now aligns with session `createdAt` instead of
    `updatedAt`. Not applicable here — we don't gate anything on
    session freshness.
  - 1.6.2 adds OAuth state-parameter CSRF verification. Applies to
    OIDC/SAML SSO flows; no code changes needed.
  - 1.6 emits a deprecation warning for `oidc-provider` in favor of
    `@better-auth/oauth-provider`. The plugin still works in 1.6.x;
    migrating it is a separate cleanup.

Prep work for an upcoming header-auth fix (issue #29 follow-up).
2026-05-27 14:44:17 +05:30
Arunavo Ray b1daa65228 chore: bump version to 3.16.2 v3.16.2 2026-05-26 11:09:31 +05:30
Sean Mousseau dd1c42264e fix: stop snapshot-row zombies + flapping force-push on deleted branches (#300)
* fix: stop snapshot-row zombies + flapping force-push on deleted branches

Two bugs caused Simple-WP-Helpdesk to accumulate one orphan
"Snapshot created" job row per scheduled sync — 7 zombies in 24h
against a deleted GitHub branch (`fix/v4.0.2-webhook-comment-author`).

(1) gitea-enhanced.ts:522 created the post-snapshot job record with
status="syncing". The snapshot was already complete at that point
(createPreSyncBundleBackup had returned), and no later code path
advanced the row to a terminal status — it just lingered. Set
status="synced" to reflect reality.

(2) force-push-detection treated any Gitea branch missing from GitHub
as a "deleted" force-push. But gitea-mirror is one-way (GitHub →
Gitea), so deletions never propagate back to the Gitea mirror —
the branch lingers in Gitea forever, the missing-from-GitHub condition
holds on every subsequent sync, and detection re-fires endlessly.
Each re-fire triggered backupBeforeSync=true to take a fresh snapshot
of the exact same state we already backed up the previous cycle.

Fix: detector accepts an `acknowledgedDeletions: { branch, giteaSha }[]`
list (persisted in RepositoryMetadataState). A Gitea branch missing
from GitHub is suppressed when its current giteaSha matches an entry.
If the giteaSha later changes (branch restored, then re-deleted with
new history), the entry won't match and detection fires again — back
up the new state, then acknowledge the new SHA. After a successful
snapshot, the call site appends each just-backed-up "deleted" branch
to the acknowledged list and persists the metadata blob.

Hoists parseRepositoryMetadataState above the backup-strategy block so
both the detection-time read and post-snapshot append happen against
the same in-memory state object; the existing metadata-mirror block
just stops re-declaring it.

Adds 9 new test cases covering:
  - deleted branch suppressed when acknowledged at matching giteaSha
  - re-flagged when giteaSha differs (restored-then-redeleted)
  - mixed deletions (only matching one suppressed)
  - undefined list = back-compat (existing callers unaffected)
  - acknowledgedDeletions does not suppress "diverged"
  - metadata-state parse/serialize round-trips the new field
  - legacy metadata defaults acknowledgedDeletions to []
  - malformed entries dropped without throwing

All 76 tests in the affected suites pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: acknowledge deletion before backup, not after, to fix concurrent-sync race

When two sync invocations fire for the same repo within the same backup
window (observed in prod via UI trigger pipeline: a single click ends up
producing two parallel processWithResilience callbacks that both run
syncGiteaRepoEnhanced), the second invocation's createPreSyncBundleBackup
short-circuits (file already exists from the first), so the second
invocation never reached the acknowledge-push that lived inside the
backup try block. Then both invocations wrote metadata: invocation A
with acknowledgedDeletions=[forged], invocation B (with its stale
in-memory state) with acknowledgedDeletions=[], and B's write landed
last — overwriting A.

Net result: the deletion never got acknowledged, and the next sync
re-detected it, the next-next sync re-detected it, etc. Same flapping
behavior as before the fix.

Fix: move the acknowledge-push to run right after detection, based on
detectionResult.affectedBranches alone. Semantically correct — once
we've detected the branch is gone from GitHub, we know it's a permanent
deletion; whether THIS particular invocation took a backup or
short-circuited is orthogonal (a previous backup on disk is fine, and
in the rare case where no backup ever succeeded, the user can re-trigger
a manual backup). Both concurrent invocations now push the same entry;
both write the same metadata; race is benign.

Verified locally: 59 tests pass across force-push-detection,
gitea-enhanced, repo-backup.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:06:27 +05:30
Arunavo Ray 384fbbbe10 docs: document Header / Forward Authentication setup
Header auth has been a working feature since v2.x but was missing from
SSO-OIDC-SETUP.md, leading users to think it was dropped in the v3
rewrite (see #29). Adds a dedicated section covering env-var config,
Authentik + Authelia examples, lookup order, verification, and the
must-strip-inbound-headers security checklist.
2026-05-25 10:37:39 +05:30
Sean Mousseau 1a54010950 fix: prevent duplicate milestones & labels on every sync (#299)
Paginate /milestones and /labels with Link header + X-Total-Count fallback, and pass state=all on milestones so closed milestones aren't re-POSTed on every sync. Caches newly-created items in the in-memory dedup set.
2026-05-25 08:28:40 +05:30
CrumblyLiquid 9bf22791a9 Regenerated bun.nix (#298) 2026-05-25 08:26:19 +05:30
Arunavo Ray 5d82f22b12 chore: sync version to 3.16.1 2026-05-23 20:25:16 +05:30
Sean Mousseau 6979c3bb32 fix: resume interrupted jobs after startup (not only at boot) (#297)
The previous middleware logic gated recovery behind a
once-per-process flag. After the first request handled the boot-time
recovery check, the resume codepath never fired again — even when
new interruptions appeared later in the process lifetime.

Symptom: a sync that started after server boot, crashed mid-flight
(deadlock retry hitting maxRetries, network blip, container
restart of an upstream service, etc.) and never reached the resume
codepath would sit at `inProgress=true, lastCheckpoint=never`
forever. The periodic detector kept finding the stuck row and
logging `Found 1 interrupted jobs:` on every poll (driven by the
health endpoint via `hasJobsNeedingRecovery`), but the resumer
(`resumeInterruptedJob`) was only invoked from `initializeRecovery`
which the middleware never re-called.

Fix: replace the one-shot gate with an in-flight latch
(`recoveryInFlight`) that's released in a `finally` block. Throttling
of actual recovery work is delegated to the existing 5-minute
`skipIfRecentAttempt` check inside `initializeRecovery()`, which is
the right place for it. `recoveryInitialized` is kept and only used
to control whether the "first run" log lines fire.

Secondary fix: `findInterruptedJobs` previously logged
`Found N interrupted jobs:` unconditionally on every call, including
from passive polls in `hasJobsNeedingRecovery` (which the health
endpoint and middleware probe call frequently). That produced one
log line per poll per stuck job for as long as the job stayed stuck.
Make logging opt-in via a `logFound` parameter, default off; the
active recovery cycle in `initializeRecovery()` opts in so the
operator-facing log still surfaces which jobs are being worked on.

Related: #268 (partially addressed). The PR #280 / v3.15.7 fix was
about a JS scoping bug that made the *initial* migrate call's
catch path crash before transitioning the repo to `failed`. That
was one cause of stuck `mirroring` state; this PR addresses the
follow-on issue that even when a job *is* correctly detectable as
interrupted, the post-startup recovery path never re-engages.

Adds `orchestrator-resume-after-startup.test.ts` using the
structural-source test pattern from
`gitea-mirror-failure-recovery.test.ts` so the four guarantees
(no static gate; in-flight latch released in finally; logging
opt-in; active path opts in) are enforced without heavy mocks.
v3.16.1
2026-05-23 20:08:33 +05:30
Sean Mousseau b9f14e55e2 fix: prevent duplicate issues & PRs on retry-after-deadlock + Link-header pagination (#296)
mirrorGitRepoIssuesToGitea and mirrorGitRepoPullRequestsToGitea both
had two compounding bugs that produced duplicate Gitea issues/PR-as-
issue rows on every sync against any non-SQLite backend.

(1) Pagination on existing-issues / existing-PRs / per-issue-comments
was wrong.

The loops paginated with `limit=100` and broke on `pageX.length <
itemsPerPage`. Gitea caps response size at server-side
[api].MAX_RESPONSE_ITEMS (default 50), so the very first page
already looks "short" and pagination terminated after one page. The
existing-issue and existing-PR maps were built from only ~50 items
per repo, so every issue/PR past page 1 was treated as new on every
sync and re-created via the CREATE branch.

Naive removal of the short-page break (relying only on "break on
empty page") doesn't work either: for some Gitea endpoints Gitea
returns the same data on every page when the page is past the
actual end instead of returning [], so the loop runs forever.

Fix: use the Link header (RFC 5988). If `rel="next"` is absent,
terminate pagination. Applied to: existing-issues pre-fetch,
existing-PRs pre-fetch, and per-issue comments fetch.

(2) Retry-after-deadlock duplicated issues/PRs even when the map
was correct.

Gitea's CreateIssue handler commits the issue insert in one
transaction and then deadlocks on the subsequent addLabel /
UPDATE repository transaction. The issue row is committed and
visible, but the in-memory dedup maps are never refreshed between
retries — processWithRetry re-invokes the callback, sees
existingIssue === undefined from the stale map, and creates a fresh
duplicate via httpPost.

Reproduces deterministically on MySQL (Error 1213 / 40001) and
PostgreSQL (40P01); SQLite escapes because writes serialize globally.

Fix: defensive recheck via httpGet by [GH-ISSUE #N] (issues) or
[PR #N] (pull requests) before the create call, with PATCH
fall-through when found. Applied to the issues create path AND
both create paths in the PR mirror (enriched + basic-fallback).
Also cache freshly-created items into the dedup maps after a
successful create so subsequent retries of the same per-item
callback don't lose track of it.

Adds gitea-issue-dedup-on-retry.test.ts using the structural-source
test pattern from gitea-mirror-failure-recovery.test.ts so all
guarantees are enforced without heavy module mocks (8 tests).
2026-05-23 20:08:23 +05:30
github-actions[bot] 2582988f94 chore: sync version to 3.16.0 2026-05-19 07:31:20 +00:00
ARUNAVO RAY 4ea62a9f3d chore: bump and digest-pin Bun base image to 1.3.14 (#295)
Same hardening principle as #293 for GitHub Actions: pin to an immutable
identifier so a future tag move can't silently change what we build against.

- Bumps oven/bun from 1.3.13 to 1.3.14 (released 2026-05-13)
- Pins to multi-arch digest sha256:9dba1a1b...db6f rather than just the tag
- Applies to both base and runner stages
v3.16.0
2026-05-19 12:50:37 +05:30
ARUNAVO RAY 1f60b2cf39 feat: add Change password and Change email to account dropdown (#292)
Adds an account menu under the avatar in the header with Change password and
Change email actions, each opening a small dialog. Calls Better Auth's existing
change-password / change-email endpoints — no new API routes.

- Enables `user.changeEmail` in Better Auth with `updateEmailWithoutVerification`
  since the app runs with email verification disabled and no email sender is
  wired up
- Hides Change password for SSO-only users (no `credential` provider account),
  fails open if the listAccounts probe errors
- Resolves discussion #291 ("Change user password/email")
2026-05-19 12:45:04 +05:30
ARUNAVO RAY a02865a1aa ci: pin third-party GitHub Actions to commit SHAs (#293)
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.
2026-05-19 12:44:23 +05:30
github-actions[bot] ad549dad9b chore: sync version to 3.15.12 2026-05-16 06:50:50 +00:00
ARUNAVO RAY 20103220d9 chore: prune npm overrides that are no longer load-bearing (#290)
Removed 5 overrides whose constraints have since been picked up naturally
by the transitive dep graph. Verified by removing each and confirming the
resolved version (and the dep tree) is identical to what the override
produced:

- defu ^6.1.7 → still resolves 6.1.7
- fast-xml-parser ^5.5.6 → still resolves 5.5.6
- node-forge ^1.3.3 → package not in tree at all (override was dead)
- rollup >=4.59.0 → still resolves 4.59.0
- svgo ^4.0.1 → still resolves 4.0.1

Kept overrides that are still doing real work:

- @esbuild-kit/esm-loader → npm:tsx@^4.21.0 — deliberate replacement shim
- @xmldom/xmldom ^0.8.13, devalue ^5.8.1, fast-uri ^3.1.2,
  fast-xml-builder ^1.1.7, kysely ^0.28.17 — active CVE pins (#289)
- lodash ^4.18.1 — pins to the newer 4.18.x line over the legacy 4.17.x
  that transitive deps still pull
- picomatch ^4.0.4 — without it, picomatch@2.3.2 is added as a duplicate
  copy via a transitive that asks for 2.x

Future drift would be caught by Dependabot + the weekly Docker Scout
scan; the overrides above remain because they currently affect the tree.
v3.15.12
2026-05-16 12:03:53 +05:30
ARUNAVO RAY fe2c825244 chore: bump npm overrides to patch HIGH-severity CVEs (#289)
Patches 9 Docker Scout HIGH alerts surfaced by the weekly image scan:

- @xmldom/xmldom 0.8.12 → 0.8.13 (CVE-2026-41672/3/4/5)
- devalue 5.6.4 → 5.8.1 (CVE-2026-42570)
- kysely 0.28.16 → 0.28.17 (CVE-2026-44635)
- fast-uri (new override) → ^3.1.2 (CVE-2026-6321, CVE-2026-6322)
- fast-xml-builder (new override) → ^1.1.7 (CVE-2026-44665)

All five resolve to fixed versions after `bun install`. Tests and astro
build pass locally.

Remaining open Docker Scout alerts (git-lfs Go stdlib, gnutls28, nghttp2)
are base-image or upstream-binary issues, not addressable via npm.
2026-05-16 11:42:53 +05:30
github-actions[bot] 4b858a0251 chore: sync version to 3.15.11 2026-05-16 04:28:38 +00:00
Eduardo Riguetto (Kralot) 585d2ceb84 fix: reconcile metadata on every sync instead of once per repo (#287)
Previously, mirror of issues/pull-requests/labels/milestones was
guarded by !metadataState.components.<component>, so once a repo had
been mirrored the metadata path was permanently skipped with logs
like "Issues already mirrored; skipping to avoid duplicates".

This meant title changes, new comments, label updates and milestone
edits on the source GitHub repo were never propagated, even when the
user explicitly clicked Sync.

The underlying mirror* functions already handle idempotent updates:
issues and PRs are matched by [GH-ISSUE #N] / [GH-PR #N] markers in
the title and PATCHed in place, labels are deduped by name, milestones
by title. The releases path already runs unconditionally for the same
reason ("always allowed to rerun for updates"); aligning the other
metadata paths with it.

Touched: mirrorGithubRepoToGitea, mirrorGitHubRepoToGiteaOrg, and the
syncGiteaRepoEnhanced path in gitea-enhanced.ts. Updated the
"already-synced-repo" test to assert reconciliation runs on resync.
v3.15.11
2026-05-16 09:40:48 +05:30
Eduardo Riguetto (Kralot) 7c1f24dc2f fix: include organization_member in /user/repos affiliation (#286)
Affiliation was set to "owner,collaborator", omitting repos owned
by orgs the user belongs to. As a result:

- main sync, scheduler, and cleanup never saw org repos
- orgs appeared empty unless manually re-added via /api/sync/organization
- restart archived previously-mirrored org repos as orphans

GitHub's API default is owner,collaborator,organization_member;
restoring it fixes both symptoms with no other code changes.
2026-05-16 09:40:40 +05:30
github-actions[bot] 680b374c84 chore: sync version to 3.15.10 2026-05-04 08:37:38 +00:00
ARUNAVO RAY 088467a57d feat: add option to exclude collaborator repos from import (closes #279) (#283)
GitHub's listForAuthenticatedUser defaults to returning every repo the
user has access to (owner + collaborator + organization_member), which
imports a lot of noise for users who only want their own repos.

Adds an `includeCollaboratorRepos` toggle, defaulting to true to preserve
existing behavior. When disabled, the affiliation filter scopes the API
call to "owner" only.

The cleanup service overrides the filter to always include collaborator
repos when computing the "what's still on GitHub" list. Without this,
toggling the option off would mark previously-mirrored collab repos as
orphaned and archive/delete them from Gitea.

Wired through the schema, both UI<->DB mappers, the env-config loader
(with new INCLUDE_COLLABORATOR_REPOS env var), and the settings UI.
v3.15.10
2026-05-04 14:00:10 +05:30
github-actions[bot] adb436444e chore: sync version to 3.15.9 2026-05-04 04:50:49 +00:00
ARUNAVO RAY cc635485f0 feat: surface auto-mirror toggle in automation settings (refs #278) (#282)
The fix in v3.15.8 made scheduleConfig.autoMirror an independent trigger
in the scheduler, but it remained reachable only via the AUTO_MIRROR_REPOS
env var. This adds a UI checkbox under the Automatic Syncing section so
the option can be toggled per-config without touching the environment.

The toggle is conditional on scheduling being enabled (since auto-mirror
without a scheduler is meaningless) and is independent of the existing
"Auto-mirror new starred repositories" toggle in GitHub settings. Together
they cover the full owned/starred matrix that the scheduler already
supports.

Plumbing: config-mapper.ts now round-trips autoMirror through the UI/DB
boundary, and ScheduleConfig in types/config.ts gets the matching field.
No schema or migration change — autoMirror was already in the zod schema.
v3.15.9
2026-05-04 10:14:09 +05:30
github-actions[bot] 6f343de5fd chore: sync version to 3.15.8 2026-05-04 03:49:44 +00:00
ARUNAVO RAY a18f262ca7 fix: make autoMirrorStarred actually trigger auto-mirror (fixes #278) (#281)
The "Auto-mirror new starred repositories" checkbox in the GitHub settings
was a filter layered on top of scheduleConfig.autoMirror, which itself is
only settable via the AUTO_MIRROR_REPOS env var (no UI). So users who
checked the box saw their starred repos auto-imported but never mirrored.

Treat autoMirror and autoMirrorStarred as independent triggers in the
scheduler: autoMirror covers owned (and self-starred) repos, autoMirrorStarred
covers repos starred from other owners. Either flag on its own is enough
to enter the auto-mirror phase, and the filter scopes the work accordingly.

Also normalize the owner comparison to lowercase since GitHub usernames are
case-insensitive — previously a self-starred repo whose stored owner casing
differed from the configured owner would be misclassified as a third-party
star.

Behavior change worth flagging in release notes: anyone who currently has
the starred checkbox on (broken state) will start getting starred repos
mirrored on upgrade. AUTO_MIRROR_REPOS=true users see no change.
v3.15.8
2026-05-04 09:12:57 +05:30
Arunavo Ray 3798456f5d chore: bump version to 3.15.7 v3.15.7 2026-05-04 08:20:26 +05:30
ARUNAVO RAY 73f1609117 fix: unstick repos in 'mirroring' on transient errors (fixes #268) (#280)
* fix: hoist migrateSucceeded above try so catch can update DB on failure (fixes #268)

`let migrateSucceeded` was declared inside the try block of
mirrorGithubRepoToGitea and mirrorGitHubRepoToGiteaOrg, but the catch
block referenced it. Block-scoping made it invisible to catch, so any
error inside the try (network timeout, transient 5xx, etc.) crashed the
catch with `ReferenceError: migrateSucceeded is not defined` before
reaching the DB update that marks the repo "failed". Result: repos
stuck in "mirroring" forever with no entry in the activity log.

Hoisting the declaration above the try restores the intended behavior:
catch updates the repo to failed, clears mirroredLocation when migrate
hadn't succeeded, writes a failed activity-log entry, and re-throws
with the original error message preserved.

TypeScript was flagging this as "Cannot find name 'migrateSucceeded'"
but esbuild stripped the types during build, so the bug shipped.

* test: replace integration test with structural source check (#268)

The behavioral version of this regression test passed locally but
failed in CI because of mock.module pollution between files: bun's
mock.module is process-wide, so my mock for @/lib/gitea-enhanced
leaked into gitea-enhanced.test.ts (its real-module assertions saw
my null-returning mocks), and gitea-enhanced.test.ts's own
@/lib/http-client mock could supersede mine depending on file
discovery order, causing my mirrorGithubRepoToGitea call to not
throw at all in CI.

Replace with a structural assertion that reads gitea.ts and verifies
`let migrateSucceeded` is declared before the outermost try in both
mirrorGithubRepoToGitea and mirrorGitHubRepoToGiteaOrg. Verified the
new test fails on the pre-fix source with a clear error message
pointing to issue #268, and passes on the fixed source.
2026-05-04 08:08:22 +05:30
Arunavo Ray 588567931a chore: bump version to 3.15.6 v3.15.6 2026-04-26 13:43:45 +05:30
Arunavo Ray 5c1317c759 feat: warn when Forgejo destination has known mirror-credential bug (refs #263)
Forgejo < 15.0.0 silently discards auth_username/auth_password sent to
/api/v1/repos/migrate, causing subsequent pull-mirror sync of private repos
to fail with `terminal prompts disabled`. Fix landed upstream in Forgejo
v15.0.0 via codeberg.org/forgejo/forgejo/pulls/11909 and was not backported
to v12/v13/v14.

Test-connection endpoint now also probes /api/v1/version, detects Forgejo
via the `+gitea-` suffix, and surfaces a warning Alert in the Gitea config
form when the connected server reports a major version below 15.
2026-04-26 13:43:40 +05:30
Arunavo Ray 5f1c37b320 fix: don't gate dashboard on optional username fields (refs #271, v3.15.5)
The useConfigStatus hook treated `githubConfig.username` and
`giteaConfig.username` as required for the dashboard to render. In
practice neither is required at runtime — the GitHub token is
self-authenticating via listForAuthenticatedUser, and a Gitea username
isn't needed under single-org or flat mirror strategies.

Users who configured via env vars without GITHUB_USERNAME / GITEA_USERNAME
set (or who left those blank in the form, which is only client-side
`required`) ended up with empty strings in their config row. Mirroring
ran fine — tokens alone are sufficient — but the dashboard refused to
fetch and rendered all zeros because useConfigStatus failed the gate.

Drop the username checks from the gate. The `githubOwner` field is still
exported for consumers that want to display an owner; only the gate is
relaxed. Cache-hit and fresh-fetch branches both updated.
v3.15.5
2026-04-22 19:21:13 +05:30
Arunavo Ray 083b342f38 ci: bump bun 1.3.10/1.3.12 → 1.3.13 across CI and runtime
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.
2026-04-22 08:39:37 +05:30
Arunavo Ray 92bb38b122 chore: bump version to 3.15.4 v3.15.4 2026-04-22 08:11:17 +05:30