Compare commits

...

6 Commits

Author SHA1 Message Date
ARUNAVO RAY
be7daac5fb ci: automate release version from tag (#211) 2026-03-05 09:34:49 +05:30
dependabot[bot]
e32b7af5eb build(deps): bump svgo (#210)
Bumps the npm_and_yarn group with 1 update in the /www directory: [svgo](https://github.com/svg/svgo).


Updates `svgo` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 4.0.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 08:53:34 +05:30
ARUNAVO RAY
d0693206c3 feat: selective starred repo mirroring with autoMirrorStarred toggle (#208)
* feat: add autoMirrorStarred toggle for selective starred repo mirroring (#205)

Add `githubConfig.autoMirrorStarred` (default: false) to control whether
starred repos are included in automatic mirroring operations. Manual
per-repo actions always work regardless of this toggle.

Bug fixes:
- Cleanup service no longer orphans starred repos when includeStarred is
  disabled (prevents data loss)
- First-boot auto-start now gates initial mirror behind autoMirror config
  (previously mirrored everything unconditionally)
- "Mirror All" button now respects autoMirrorStarred setting
- Bulk mirror and getAvailableActions now include pending-approval status

Changes span schema, config mapping, env loader, scheduler, cleanup
service, UI settings toggle, and repository components.

* fix: log activity when repos are auto-imported during scheduled sync

Auto-discovered repositories (including newly starred ones) were inserted
into the database without creating activity log entries, so they appeared
in the dashboard but not in the activity log.

* ci: set 10-minute timeout on all CI jobs
2026-03-04 08:22:44 +05:30
Arunavo Ray
b079070c30 ci: also exclude helm/** from app CI workflows 2026-03-02 16:28:04 +05:30
Arunavo Ray
e68e9c38a8 ci: skip app CI workflows for www-only changes
Add www/** to paths-ignore in astro-build-test, e2e-tests, and
nix-build workflows. docker-build and helm-test already use positive
path filters and were unaffected.
2026-03-02 16:25:54 +05:30
Arunavo Ray
534150ecf9 chore(www): update website content, fix build, add Helm/Nix install methods
- Update softwareVersion from 3.9.2 to 3.11.0
- Add Helm and Nix installation tabs to Getting Started section
- Fix Helm instructions to use local chart path (no published repo)
- Update Features section: add Metadata Preservation, Force-Push Protection, Git LFS Support
- Remove unused @radix-ui/react-icons import from Hero.tsx and dependency from package.json
- Update structured data featureList with newer capabilities
2026-03-02 16:23:32 +05:30
24 changed files with 280 additions and 71 deletions

View File

@@ -43,6 +43,8 @@ This workflow builds Docker images on pushes and pull requests, and pushes to Gi
- Skips registry push for fork PRs (avoids package write permission failures)
- Uses build caching to speed up builds
- Creates multiple tags for each image (latest, semver, sha)
- Auto-syncs `package.json` version from `v*` tags during release builds
- Validates release tags use semver format before building
### Docker Security Scan (`docker-scan.yml`)

View File

@@ -6,11 +6,15 @@ on:
paths-ignore:
- 'README.md'
- 'docs/**'
- 'www/**'
- 'helm/**'
pull_request:
branches: [ '*' ]
paths-ignore:
- 'README.md'
- 'docs/**'
- 'www/**'
- 'helm/**'
permissions:
contents: read
@@ -20,6 +24,7 @@ jobs:
build-and-test:
name: Build and Test Astro Project
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository

View File

@@ -36,6 +36,7 @@ env:
jobs:
docker:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
@@ -76,13 +77,34 @@ jobs:
id: tag_version
run: |
if [[ $GITHUB_REF == refs/tags/v* ]]; then
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
echo "Using version tag: ${GITHUB_REF#refs/tags/}"
TAG_VERSION="${GITHUB_REF#refs/tags/}"
if [[ ! "$TAG_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Release tag '${TAG_VERSION}' is invalid. Expected semver tag format like v1.2.3 or v1.2.3-rc.1"
exit 1
fi
APP_VERSION="${TAG_VERSION#v}"
echo "VERSION=${TAG_VERSION}" >> $GITHUB_OUTPUT
echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_OUTPUT
echo "Using version tag: ${TAG_VERSION}"
else
echo "VERSION=latest" >> $GITHUB_OUTPUT
echo "APP_VERSION=dev" >> $GITHUB_OUTPUT
echo "No version tag, using 'latest'"
fi
# Keep version files aligned automatically for tag-based releases
- name: Sync app version from release tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
VERSION="${{ steps.tag_version.outputs.APP_VERSION }}"
echo "Syncing package.json version to ${VERSION}"
jq --arg version "${VERSION}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
echo "Version sync diff (package.json):"
git --no-pager diff -- package.json
# Extract metadata for Docker
- name: Extract Docker metadata
id: meta

View File

@@ -8,6 +8,8 @@ on:
- "docs/**"
- "CHANGELOG.md"
- "LICENSE"
- "www/**"
- "helm/**"
pull_request:
branches: ["*"]
paths-ignore:
@@ -15,6 +17,8 @@ on:
- "docs/**"
- "CHANGELOG.md"
- "LICENSE"
- "www/**"
- "helm/**"
workflow_dispatch:
inputs:
debug_enabled:
@@ -42,7 +46,7 @@ jobs:
e2e-tests:
name: E2E Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 10
steps:
- name: Checkout repository

View File

@@ -21,6 +21,7 @@ jobs:
yamllint:
name: Lint YAML
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
@@ -35,6 +36,7 @@ jobs:
helm-template:
name: Helm lint & template
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Setup Helm

View File

@@ -5,8 +5,18 @@ on:
branches: [main, nix]
tags:
- 'v*'
paths-ignore:
- 'README.md'
- 'docs/**'
- 'www/**'
- 'helm/**'
pull_request:
branches: [main]
paths-ignore:
- 'README.md'
- 'docs/**'
- 'www/**'
- 'helm/**'
permissions:
contents: read
@@ -14,6 +24,7 @@ permissions:
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

View File

@@ -310,26 +310,24 @@ bunx tsc --noEmit
## Release Process
1. **Update version**:
```bash
npm version patch # or minor/major
```
1. **Choose release version** (`X.Y.Z`) and update `CHANGELOG.md`
2. **Update CHANGELOG.md**
3. **Build and test**:
2. **Build and test**:
```bash
bun run build
bun test
```
4. **Create release**:
3. **Create release tag** (semver format required):
```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```
5. **Create GitHub release**
4. **Create GitHub release**
5. **CI version sync (automatic)**:
- On `v*` tags, release CI updates `package.json` version in the build context from the tag (`vX.Y.Z` -> `X.Y.Z`), so Docker release images always report the correct app version.
## Contributing

View File

@@ -78,6 +78,7 @@ Settings for connecting to and configuring GitHub repository sources.
| Variable | Description | Default | Options |
|----------|-------------|---------|---------|
| `SKIP_STARRED_ISSUES` | Enable lightweight mode for starred repos (skip issues) | `false` | `true`, `false` |
| `AUTO_MIRROR_STARRED` | Automatically mirror starred repos during scheduled syncs and "Mirror All". When `false`, starred repos are imported for browsing but must be mirrored individually. | `false` | `true`, `false` |
## Gitea Configuration

View File

@@ -83,6 +83,7 @@ export function ConfigTabs() {
advancedOptions: {
skipForks: false,
starredCodeOnly: false,
autoMirrorStarred: false,
},
});
const { user } = useAuth();

View File

@@ -287,6 +287,31 @@ export function GitHubMirrorSettings({
</div>
</div>
{/* Auto-mirror starred repos toggle */}
{githubConfig.mirrorStarred && (
<div className="mt-4">
<div className="flex items-start space-x-3">
<Checkbox
id="auto-mirror-starred"
checked={advancedOptions.autoMirrorStarred ?? false}
onCheckedChange={(checked) => handleAdvancedChange('autoMirrorStarred', !!checked)}
/>
<div className="space-y-0.5 flex-1">
<Label
htmlFor="auto-mirror-starred"
className="text-sm font-normal cursor-pointer flex items-center gap-2"
>
<Star className="h-3.5 w-3.5" />
Auto-mirror new starred repositories
</Label>
<p className="text-xs text-muted-foreground">
When disabled, starred repos are imported for browsing but not automatically mirrored. You can still mirror individual repos manually.
</p>
</div>
</div>
</div>
)}
{/* Duplicate name handling for starred repos */}
{githubConfig.mirrorStarred && (
<div className="mt-4 space-y-2">

View File

@@ -56,7 +56,7 @@ export default function Repository() {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const { user } = useAuth();
const { registerRefreshCallback, isLiveEnabled } = useLiveRefresh();
const { isGitHubConfigured, isFullyConfigured } = useConfigStatus();
const { isGitHubConfigured, isFullyConfigured, autoMirrorStarred, githubOwner } = useConfigStatus();
const { navigationKey } = useNavigation();
const { filter, setFilter } = useFilterParams({
searchTerm: "",
@@ -233,10 +233,12 @@ export default function Repository() {
// Filter out repositories that are already mirroring, mirrored, or ignored
const eligibleRepos = repositories.filter(
(repo) =>
repo.status !== "mirroring" &&
repo.status !== "mirrored" &&
repo.status !== "mirroring" &&
repo.status !== "mirrored" &&
repo.status !== "ignored" && // Skip ignored repositories
repo.id
repo.id &&
// Skip starred repos from other owners when autoMirrorStarred is disabled
!(repo.isStarred && !autoMirrorStarred && repo.owner !== githubOwner)
);
if (eligibleRepos.length === 0) {
@@ -292,7 +294,7 @@ export default function Repository() {
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
const eligibleRepos = selectedRepos.filter(
repo => repo.status === "imported" || repo.status === "failed"
repo => repo.status === "imported" || repo.status === "failed" || repo.status === "pending-approval"
);
if (eligibleRepos.length === 0) {
@@ -301,7 +303,7 @@ export default function Repository() {
}
const repoIds = eligibleRepos.map(repo => repo.id as string);
setLoadingRepoIds(prev => {
const newSet = new Set(prev);
repoIds.forEach(id => newSet.add(id));
@@ -937,7 +939,7 @@ export default function Repository() {
const actions = [];
// Check if any selected repos can be mirrored
if (selectedRepos.some(repo => repo.status === "imported" || repo.status === "failed")) {
if (selectedRepos.some(repo => repo.status === "imported" || repo.status === "failed" || repo.status === "pending-approval")) {
actions.push('mirror');
}
@@ -975,7 +977,7 @@ export default function Repository() {
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
return {
mirror: selectedRepos.filter(repo => repo.status === "imported" || repo.status === "failed").length,
mirror: selectedRepos.filter(repo => repo.status === "imported" || repo.status === "failed" || repo.status === "pending-approval").length,
sync: selectedRepos.filter(repo => repo.status === "mirrored" || repo.status === "synced").length,
rerunMetadata: selectedRepos.filter(repo => ["mirrored", "synced", "archived"].includes(repo.status)).length,
retry: selectedRepos.filter(repo => repo.status === "failed").length,

View File

@@ -9,6 +9,8 @@ interface ConfigStatus {
isFullyConfigured: boolean;
isLoading: boolean;
error: string | null;
autoMirrorStarred: boolean;
githubOwner: string;
}
// Cache to prevent duplicate API calls across components
@@ -33,6 +35,8 @@ export function useConfigStatus(): ConfigStatus {
isFullyConfigured: false,
isLoading: true,
error: null,
autoMirrorStarred: false,
githubOwner: '',
});
// Track if this hook has already checked config to prevent multiple calls
@@ -46,6 +50,8 @@ export function useConfigStatus(): ConfigStatus {
isFullyConfigured: false,
isLoading: false,
error: 'No user found',
autoMirrorStarred: false,
githubOwner: '',
});
return;
}
@@ -78,6 +84,8 @@ export function useConfigStatus(): ConfigStatus {
isFullyConfigured,
isLoading: false,
error: null,
autoMirrorStarred: configResponse?.advancedOptions?.autoMirrorStarred ?? false,
githubOwner: configResponse?.githubConfig?.username ?? '',
});
return;
}
@@ -119,6 +127,8 @@ export function useConfigStatus(): ConfigStatus {
isFullyConfigured,
isLoading: false,
error: null,
autoMirrorStarred: configResponse?.advancedOptions?.autoMirrorStarred ?? false,
githubOwner: configResponse?.githubConfig?.username ?? '',
});
hasCheckedRef.current = true;
@@ -129,6 +139,8 @@ export function useConfigStatus(): ConfigStatus {
isFullyConfigured: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Failed to check configuration',
autoMirrorStarred: false,
githubOwner: '',
});
hasCheckedRef.current = true;
}

View File

@@ -29,6 +29,7 @@ export const githubConfigSchema = z.object({
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
defaultOrg: z.string().optional(),
starredCodeOnly: z.boolean().default(false),
autoMirrorStarred: z.boolean().default(false),
skipStarredIssues: z.boolean().optional(), // Deprecated: kept for backward compatibility, use starredCodeOnly instead
starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(),
});

View File

@@ -22,6 +22,7 @@ interface EnvConfig {
preserveOrgStructure?: boolean;
onlyMirrorOrgs?: boolean;
starredCodeOnly?: boolean;
autoMirrorStarred?: boolean;
starredReposOrg?: string;
starredReposMode?: 'dedicated-org' | 'preserve-owner';
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
@@ -113,6 +114,7 @@ function parseEnvConfig(): EnvConfig {
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
autoMirrorStarred: process.env.AUTO_MIRROR_STARRED === 'true',
starredReposOrg: process.env.STARRED_REPOS_ORG,
starredReposMode: process.env.STARRED_REPOS_MODE as 'dedicated-org' | 'preserve-owner',
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
@@ -264,6 +266,7 @@ export async function initializeConfigFromEnv(): Promise<void> {
mirrorStrategy,
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,
autoMirrorStarred: envConfig.github.autoMirrorStarred ?? existingConfig?.[0]?.githubConfig?.autoMirrorStarred ?? false,
};
// Build Gitea config

View File

@@ -79,6 +79,13 @@ async function identifyOrphanedRepositories(config: any): Promise<any[]> {
return false;
}
// If starred repos are not being fetched from GitHub, we can't determine
// if a starred repo is orphaned - skip it to prevent data loss
if (repo.isStarred && !config.githubConfig?.includeStarred) {
console.log(`[Repository Cleanup] Skipping starred repo ${repo.fullName} - starred repos not being fetched from GitHub`);
return false;
}
const githubRepo = githubReposByFullName.get(repo.fullName);
if (!githubRepo) {
return true;

View File

@@ -13,6 +13,7 @@ import type { Repository } from '@/lib/db/schema';
import { repoStatusEnum, repositoryVisibilityEnum } from '@/types/Repository';
import { mergeGitReposPreferStarred, normalizeGitRepoToInsert, calcBatchSizeForInsert } from '@/lib/repo-utils';
import { isMirrorableGitHubRepo } from '@/lib/repo-eligibility';
import { createMirrorJob } from '@/lib/helpers';
let schedulerInterval: NodeJS.Timeout | null = null;
let isSchedulerRunning = false;
@@ -128,6 +129,19 @@ async function runScheduledSync(config: any): Promise<void> {
.onConflictDoNothing({ target: [repositories.userId, repositories.normalizedFullName] });
}
console.log(`[Scheduler] Successfully imported ${newRepos.length} new repositories for user ${userId}`);
// Log activity for each newly imported repo
for (const repo of newRepos) {
const sourceLabel = repo.isStarred ? 'starred' : 'owned';
await createMirrorJob({
userId,
repositoryName: repo.fullName,
message: `Auto-imported ${sourceLabel} repository: ${repo.fullName}`,
details: `Repository ${repo.fullName} was discovered and imported during scheduled sync.`,
status: 'imported',
skipDuplicateEvent: true,
});
}
} else {
console.log(`[Scheduler] No new repositories found for user ${userId}`);
}
@@ -176,7 +190,7 @@ async function runScheduledSync(config: any): Promise<void> {
if (scheduleConfig.autoMirror) {
try {
console.log(`[Scheduler] Auto-mirror enabled - checking for repositories to mirror for user ${userId}...`);
const reposNeedingMirror = await db
let reposNeedingMirror = await db
.select()
.from(repositories)
.where(
@@ -190,6 +204,19 @@ async function runScheduledSync(config: any): Promise<void> {
)
);
// Filter out starred repos from auto-mirror when autoMirrorStarred is disabled
if (!config.githubConfig?.autoMirrorStarred) {
const githubOwner = config.githubConfig?.owner || '';
const beforeCount = reposNeedingMirror.length;
reposNeedingMirror = reposNeedingMirror.filter(
repo => !repo.isStarred || repo.owner === githubOwner
);
const skippedCount = beforeCount - reposNeedingMirror.length;
if (skippedCount > 0) {
console.log(`[Scheduler] Skipped ${skippedCount} starred repositories from auto-mirror (autoMirrorStarred is disabled)`);
}
}
if (reposNeedingMirror.length > 0) {
console.log(`[Scheduler] Found ${reposNeedingMirror.length} repositories that need initial mirroring`);
@@ -484,6 +511,19 @@ async function performInitialAutoStart(): Promise<void> {
.onConflictDoNothing({ target: [repositories.userId, repositories.normalizedFullName] });
}
console.log(`[Scheduler] Successfully imported ${reposToImport.length} repositories`);
// Log activity for each newly imported repo
for (const repo of reposToImport) {
const sourceLabel = repo.isStarred ? 'starred' : 'owned';
await createMirrorJob({
userId: config.userId,
repositoryName: repo.fullName,
message: `Auto-imported ${sourceLabel} repository: ${repo.fullName}`,
details: `Repository ${repo.fullName} was discovered and imported during auto-start.`,
status: 'imported',
skipDuplicateEvent: true,
});
}
} else {
console.log(`[Scheduler] No new repositories to import for user ${config.userId}`);
}
@@ -491,7 +531,7 @@ async function performInitialAutoStart(): Promise<void> {
if (skippedDisabledCount > 0) {
console.log(`[Scheduler] Skipped ${skippedDisabledCount} disabled GitHub repositories for user ${config.userId}`);
}
// Check if we already have mirrored repositories (indicating this isn't first run)
const mirroredRepos = await db
.select()
@@ -534,8 +574,34 @@ async function performInitialAutoStart(): Promise<void> {
}
// Step 2: Trigger mirror for all repositories that need mirroring
// Only auto-mirror if autoMirror is enabled in schedule config
if (!config.scheduleConfig?.autoMirror) {
console.log(`[Scheduler] Step 2: Skipping initial mirror - autoMirror is disabled for user ${config.userId}`);
// Still update schedule config timestamps
const currentTime2 = new Date();
const intervalSource2 = config.scheduleConfig?.interval ||
config.giteaConfig?.mirrorInterval ||
'8h';
const interval2 = parseScheduleInterval(intervalSource2);
const nextRun2 = new Date(currentTime2.getTime() + interval2);
await db.update(configs).set({
scheduleConfig: {
...config.scheduleConfig,
enabled: true,
lastRun: currentTime2,
nextRun: nextRun2,
},
updatedAt: currentTime2,
}).where(eq(configs.id, config.id));
console.log(`[Scheduler] Scheduling enabled for user ${config.userId}, next sync at ${nextRun2.toISOString()}`);
continue;
}
console.log(`[Scheduler] Step 2: Triggering mirror for repositories that need mirroring...`);
const reposNeedingMirror = await db
let reposNeedingMirror = await db
.select()
.from(repositories)
.where(
@@ -548,7 +614,20 @@ async function performInitialAutoStart(): Promise<void> {
)
)
);
// Filter out starred repos from auto-mirror when autoMirrorStarred is disabled
if (!config.githubConfig?.autoMirrorStarred) {
const githubOwner = config.githubConfig?.owner || '';
const beforeCount = reposNeedingMirror.length;
reposNeedingMirror = reposNeedingMirror.filter(
repo => !repo.isStarred || repo.owner === githubOwner
);
const skippedCount = beforeCount - reposNeedingMirror.length;
if (skippedCount > 0) {
console.log(`[Scheduler] Skipped ${skippedCount} starred repositories from initial auto-mirror (autoMirrorStarred is disabled)`);
}
}
if (reposNeedingMirror.length > 0) {
console.log(`[Scheduler] Found ${reposNeedingMirror.length} repositories that need mirroring`);

View File

@@ -56,6 +56,7 @@ export function mapUiToDbConfig(
// Advanced options
starredCodeOnly: advancedOptions.starredCodeOnly,
autoMirrorStarred: advancedOptions.autoMirrorStarred ?? false,
};
// Map Gitea config to match database schema
@@ -172,6 +173,7 @@ export function mapDbToUiConfig(dbConfig: any): {
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
// Support both old (skipStarredIssues) and new (starredCodeOnly) field names for backward compatibility
starredCodeOnly: dbConfig.githubConfig?.starredCodeOnly ?? (dbConfig.githubConfig as any)?.skipStarredIssues ?? false,
autoMirrorStarred: dbConfig.githubConfig?.autoMirrorStarred ?? false,
};
return {

View File

@@ -75,6 +75,7 @@ export interface MirrorOptions {
export interface AdvancedOptions {
skipForks: boolean;
starredCodeOnly: boolean;
autoMirrorStarred?: boolean;
}
export interface SaveConfigApiRequest {

View File

@@ -11,7 +11,6 @@
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"@astrojs/react": "^4.4.2",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-slot": "^1.2.4",
"@splinetool/react-spline": "^4.1.0",
"@splinetool/runtime": "^1.12.60",

28
www/pnpm-lock.yaml generated
View File

@@ -14,9 +14,6 @@ importers:
'@astrojs/react':
specifier: ^4.4.2
version: 4.4.2(@types/node@24.7.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(jiti@2.6.1)(lightningcss@1.31.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@19.2.4)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
@@ -674,11 +671,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-icons@1.3.2':
resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
@@ -1951,8 +1943,8 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
sax@1.5.0:
resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
engines: {node: '>=11.0.0'}
scheduler@0.27.0:
@@ -2020,8 +2012,8 @@ packages:
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
svgo@4.0.0:
resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
svgo@4.0.1:
resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==}
engines: {node: '>=16'}
hasBin: true
@@ -2828,10 +2820,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-icons@1.3.2(react@19.2.4)':
dependencies:
react: 19.2.4
'@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -3197,7 +3185,7 @@ snapshots:
semver: 7.7.4
shiki: 3.22.0
smol-toml: 1.6.0
svgo: 4.0.0
svgo: 4.0.1
tinyexec: 1.0.2
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.8.3)
@@ -4564,7 +4552,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
sax@1.4.4: {}
sax@1.5.0: {}
scheduler@0.27.0: {}
@@ -4660,7 +4648,7 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
svgo@4.0.0:
svgo@4.0.1:
dependencies:
commander: 11.1.0
css-select: 5.2.2
@@ -4668,7 +4656,7 @@ snapshots:
css-what: 6.2.2
csso: 5.0.5
picocolors: 1.1.1
sax: 1.4.4
sax: 1.5.0
tailwind-merge@3.5.0: {}

View File

@@ -1,11 +1,11 @@
---
import {
RefreshCw,
Building2,
FolderTree,
Activity,
Lock,
Heart,
import {
RefreshCw,
FileText,
ShieldCheck,
Activity,
Lock,
HardDrive,
} from 'lucide-react';
const features = [
@@ -17,37 +17,37 @@ const features = [
iconColor: "text-primary"
},
{
title: "Bulk Operations",
description: "Mirror entire organizations or user accounts with a single configuration.",
icon: Building2,
title: "Metadata Preservation",
description: "Mirror issues, pull requests, releases, labels, milestones, and wiki pages alongside your code.",
icon: FileText,
gradient: "from-accent/10 to-accent-teal/10",
iconColor: "text-accent"
},
{
title: "Preserve Structure",
description: "Maintain your GitHub organization structure or customize how repos are organized.",
icon: FolderTree,
title: "Force-Push Protection",
description: "Detect upstream force-pushes and automatically snapshot repos before destructive changes.",
icon: ShieldCheck,
gradient: "from-accent-teal/10 to-primary/10",
iconColor: "text-accent-teal"
},
{
title: "Real-time Status",
description: "Monitor mirror progress with live updates and detailed activity logs.",
title: "Real-time Dashboard",
description: "Monitor mirror progress with live updates, activity logs, and per-repo status tracking.",
icon: Activity,
gradient: "from-accent-coral/10 to-primary/10",
iconColor: "text-accent-coral"
},
{
title: "Secure & Private",
description: "Self-hosted solution keeps your code on your infrastructure with full control.",
title: "Secure & Self-Hosted",
description: "Tokens encrypted at rest with AES-256-GCM. Your code stays on your infrastructure.",
icon: Lock,
gradient: "from-accent-purple/10 to-primary/10",
iconColor: "text-accent-purple"
},
{
title: "Open Source",
description: "Free, transparent, and community-driven development. Contribute and customize.",
icon: Heart,
title: "Git LFS Support",
description: "Mirror large files and binary assets alongside your repositories with full LFS support.",
icon: HardDrive,
gradient: "from-primary/10 to-accent-purple/10",
iconColor: "text-primary"
}

View File

@@ -1,6 +1,5 @@
import { Button } from "./ui/button";
import { ArrowRight, Shield, RefreshCw, HardDrive } from "lucide-react";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import React, { Suspense } from 'react';
const Spline = React.lazy(() => import('@splinetool/react-spline'));

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Copy, Check, Terminal, Container, Cloud } from 'lucide-react';
import { Copy, Check, Terminal, Container, Cloud, Ship, Snowflake } from 'lucide-react';
type InstallMethod = 'docker' | 'manual' | 'proxmox';
type InstallMethod = 'docker' | 'helm' | 'nix' | 'manual' | 'proxmox';
export function Installation() {
const [activeMethod, setActiveMethod] = useState<InstallMethod>('docker');
@@ -37,6 +37,50 @@ export function Installation() {
}
]
},
helm: {
icon: Ship,
title: "Helm",
description: "Deploy to Kubernetes",
steps: [
{
title: "Clone the repository",
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git && cd gitea-mirror",
id: "helm-clone"
},
{
title: "Install the chart",
command: "helm upgrade --install gitea-mirror ./helm/gitea-mirror \\\n --namespace gitea-mirror --create-namespace",
id: "helm-install"
},
{
title: "Access the application",
command: "kubectl port-forward svc/gitea-mirror 4321:4321 -n gitea-mirror",
id: "helm-access"
}
]
},
nix: {
icon: Snowflake,
title: "Nix",
description: "Zero-config with Nix flakes",
steps: [
{
title: "Run directly with Nix",
command: "nix run github:RayLabsHQ/gitea-mirror",
id: "nix-run"
},
{
title: "Or install to your profile",
command: "nix profile install github:RayLabsHQ/gitea-mirror",
id: "nix-install"
},
{
title: "Access the application",
command: "# Open http://localhost:4321 in your browser",
id: "nix-access"
}
]
},
manual: {
icon: Terminal,
title: "Manual",

View File

@@ -39,7 +39,7 @@ const structuredData = {
name: "RayLabs",
url: "https://github.com/RayLabsHQ",
},
softwareVersion: "3.9.2",
softwareVersion: "3.11.0",
screenshot: [
`${siteUrl}/assets/dashboard.png`,
`${siteUrl}/assets/repositories.png`,
@@ -49,8 +49,9 @@ const structuredData = {
"Automated scheduled backups",
"Self-hosted (full data ownership)",
"Metadata preservation (issues, PRs, releases, wiki)",
"Docker support",
"Multi-repository backup",
"Force-push protection with smart detection",
"Docker, Helm, Nix, and Proxmox support",
"Multi-repository and organization backup",
"Git LFS support",
"Free and open source",
],