mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-14 22:43:02 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be7daac5fb | ||
|
|
e32b7af5eb | ||
|
|
d0693206c3 | ||
|
|
b079070c30 | ||
|
|
e68e9c38a8 | ||
|
|
534150ecf9 |
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -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`)
|
||||
|
||||
|
||||
5
.github/workflows/astro-build-test.yml
vendored
5
.github/workflows/astro-build-test.yml
vendored
@@ -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
|
||||
|
||||
26
.github/workflows/docker-build.yml
vendored
26
.github/workflows/docker-build.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/helm-test.yml
vendored
2
.github/workflows/helm-test.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/nix-build.yml
vendored
11
.github/workflows/nix-build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ export function ConfigTabs() {
|
||||
advancedOptions: {
|
||||
skipForks: false,
|
||||
starredCodeOnly: false,
|
||||
autoMirrorStarred: false,
|
||||
},
|
||||
});
|
||||
const { user } = useAuth();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface MirrorOptions {
|
||||
export interface AdvancedOptions {
|
||||
skipForks: boolean;
|
||||
starredCodeOnly: boolean;
|
||||
autoMirrorStarred?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveConfigApiRequest {
|
||||
|
||||
@@ -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
28
www/pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user