Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67f907664c | ||
|
|
be0594b28f | ||
|
|
c51fa958ba | ||
|
|
e49cf0e367 | ||
|
|
199879a64c | ||
|
|
6b8d0b3d78 | ||
|
|
72a3223c76 | ||
|
|
2c394bb722 | ||
|
|
e82aa86723 | ||
|
|
c2ea2b3cb6 | ||
|
|
a74b7e54ed | ||
|
|
3a2f8b34ab | ||
|
|
7d17194e51 | ||
|
|
7dfb867202 | ||
|
|
4478c32db1 | ||
|
|
34babd4d8c | ||
|
|
67ca35d893 | ||
|
|
4a40497ff2 | ||
|
|
9a5bc450c2 | ||
|
|
ec655b12ce | ||
|
|
c14473e3f8 | ||
|
|
f7d830ac03 | ||
|
|
68b493292e | ||
|
|
0be31d8c58 | ||
|
|
e4cf48e4a1 | ||
|
|
30110a7745 | ||
|
|
ff734d48ab | ||
|
|
38fac54b17 | ||
|
|
8f9fc55c8e | ||
|
|
1e43dc4652 | ||
|
|
83b59874a8 | ||
|
|
a1ff0433df | ||
|
|
26f78e06b8 | ||
|
|
83dd74dd52 | ||
|
|
8d22774b0a | ||
|
|
3ff05ec7f3 | ||
|
|
d211e54877 | ||
|
|
a579934539 | ||
|
|
f767a0a2b3 | ||
|
|
b1d2ea4d6d | ||
|
|
e7270a6ca3 | ||
|
|
4ac2c4323c | ||
|
|
cadeb64dce | ||
|
|
7fcce46821 | ||
|
|
64e3b18e1e | ||
|
|
b3e8c69566 | ||
|
|
f06f6695bb | ||
|
|
a000073a68 | ||
|
|
2355284d27 | ||
|
|
6f3727c47e | ||
|
|
1bb096aa93 | ||
|
|
feb13ff58a | ||
|
|
41592cb8fd | ||
|
|
8446811e8d | ||
|
|
0c41fac9c0 | ||
|
|
f18fc4bfe6 | ||
|
|
d63d55f52d | ||
|
|
2867076d71 | ||
|
|
7a935ff1b1 | ||
|
|
85b72e9366 | ||
|
|
0f700c7197 | ||
|
|
a295a73066 | ||
|
|
67cd0e2c60 | ||
|
|
c9edc2132b | ||
|
|
d4db47c76e | ||
|
|
91eb195494 | ||
|
|
e0c39b697f | ||
|
|
03fce1e299 | ||
|
|
7fc53cfad8 | ||
|
|
b33e7d5596 | ||
|
|
2f6af22e25 | ||
|
|
185946984b | ||
|
|
bcc9cf0120 | ||
|
|
2b455354f9 | ||
|
|
428c97441c | ||
|
|
edf9c14be0 | ||
|
|
5c49191d61 | ||
|
|
63a4c9359a | ||
|
|
882e147504 | ||
|
|
da75f10f26 | ||
|
|
e7758badbf | ||
|
|
69198c047a | ||
|
|
4f55cb406c | ||
|
|
c7932faecb | ||
|
|
e40eaabf4c | ||
|
|
204922570d | ||
|
|
97d98b82c6 | ||
|
|
8b81ffa975 | ||
|
|
bb57b52de3 | ||
|
|
b3aad9d80f | ||
|
|
3b8625634c | ||
|
|
bdbfa762af | ||
|
|
f922bcc618 | ||
|
|
c5b331c041 | ||
|
|
5c33a5547b | ||
|
|
537aae952d | ||
|
|
40efb9b83a | ||
|
|
06bfb49e0e | ||
|
|
b5e0c58708 | ||
|
|
74606f0a5f | ||
|
|
187ecc5d60 | ||
|
|
632bbd0d4a | ||
|
|
0b65e40784 | ||
|
|
4a8b4f6ff3 | ||
|
|
1d9dfdeb70 | ||
|
|
079df29f44 | ||
|
|
dff3cafb5e | ||
|
|
6ca7c0eec0 | ||
|
|
6ebf1916e8 | ||
|
|
91de0d1030 | ||
|
|
85bd1f4042 | ||
|
|
4a28015685 | ||
|
|
da23941369 | ||
|
|
906ce57e8c | ||
|
|
1b84c75a97 | ||
|
|
0b6b6b76bf | ||
|
|
7610a614da | ||
|
|
c28dcc209f | ||
|
|
40ee3cbc44 | ||
|
|
699a5771f5 | ||
|
|
e862714d6a | ||
|
|
716981aa04 | ||
|
|
66e3284898 | ||
|
|
695f0ff005 | ||
|
|
53f2cf36fc | ||
|
|
8ffcf3bdc6 | ||
|
|
a07af96f84 | ||
|
|
b1daa65228 | ||
|
|
dd1c42264e | ||
|
|
384fbbbe10 | ||
|
|
1a54010950 | ||
|
|
9bf22791a9 | ||
|
|
5d82f22b12 | ||
|
|
6979c3bb32 | ||
|
|
b9f14e55e2 | ||
|
|
2582988f94 | ||
|
|
4ea62a9f3d | ||
|
|
1f60b2cf39 | ||
|
|
a02865a1aa | ||
|
|
ad549dad9b | ||
|
|
20103220d9 | ||
|
|
fe2c825244 | ||
|
|
4b858a0251 | ||
|
|
585d2ceb84 | ||
|
|
7c1f24dc2f | ||
|
|
680b374c84 | ||
|
|
088467a57d | ||
|
|
adb436444e | ||
|
|
cc635485f0 | ||
|
|
6f343de5fd | ||
|
|
a18f262ca7 | ||
|
|
3798456f5d | ||
|
|
73f1609117 | ||
|
|
588567931a | ||
|
|
5c1317c759 | ||
|
|
5f1c37b320 | ||
|
|
083b342f38 | ||
|
|
92bb38b122 | ||
|
|
e7ac54a72a | ||
|
|
2ea250f081 | ||
|
|
c1712bc670 | ||
|
|
c4550196e9 | ||
|
|
8cb8fd6fe1 | ||
|
|
4b4ea9614b | ||
|
|
1644505043 | ||
|
|
e142524bfc | ||
|
|
8fac30fc02 | ||
|
|
c3b1f933b1 | ||
|
|
a839915e3e | ||
|
|
5d95f4cd39 | ||
|
|
01a3b08dac | ||
|
|
c87513b648 | ||
|
|
4f3cbc866e | ||
|
|
60548f2062 | ||
|
|
74dab43e89 | ||
|
|
01a8025140 | ||
|
|
8346748f5a | ||
|
|
38002019ea | ||
|
|
32eb27c8a6 | ||
|
|
d33b4ff64f | ||
|
|
6f2e0cbca0 | ||
|
|
95e6eb7602 | ||
|
|
f50f49fc41 | ||
|
|
5ea2abff85 | ||
|
|
9d131b9a09 | ||
|
|
5f77fceaca | ||
|
|
5d2462e5a0 | ||
|
|
0000a03ad6 | ||
|
|
d697cb2bc9 | ||
|
|
ddd071f7e5 | ||
|
|
4629ab4335 | ||
|
|
0f303c4b79 | ||
|
|
7c7c259d0a | ||
|
|
fe6bcc5288 | ||
|
|
e26ed3aa9c | ||
|
|
efb96b6e60 | ||
|
|
342cafed0e | ||
|
|
fc7c6b59d7 | ||
|
|
a77ec0447a | ||
|
|
82b5ac8160 | ||
|
|
299659eca2 | ||
|
|
6f53a3ed41 | ||
|
|
1bca7df5ab | ||
|
|
b5210c3916 | ||
|
|
755647e29c | ||
|
|
018c9d1a23 | ||
|
|
c89011819f | ||
|
|
c00d48199b | ||
|
|
de28469210 | ||
|
|
0e2f83fee0 | ||
|
|
1dd3dea231 | ||
|
|
db783c4225 | ||
|
|
8a4716bdbd | ||
|
|
9d37966c10 | ||
|
|
ac16ae56ea | ||
|
|
df3e665978 | ||
|
|
8a26764d2c | ||
|
|
ce365a706e | ||
|
|
be7daac5fb | ||
|
|
e32b7af5eb | ||
|
|
d0693206c3 | ||
|
|
b079070c30 | ||
|
|
e68e9c38a8 | ||
|
|
534150ecf9 | ||
|
|
98da7065e0 | ||
|
|
58e0194aa6 | ||
|
|
7864c46279 | ||
|
|
e3970e53e1 | ||
|
|
be46cfdffa | ||
|
|
2e00a610cb | ||
|
|
61841dd7a5 | ||
|
|
5aa0f3260d | ||
|
|
d0efa200d9 | ||
|
|
c26b5574e0 | ||
|
|
89a6372565 | ||
|
|
f40cad4713 | ||
|
|
855906d990 | ||
|
|
08da526ddd | ||
|
|
2395e14382 | ||
|
|
91c1703bb5 | ||
|
|
6a548e3dac | ||
|
|
f28ac8fa09 | ||
|
|
5e86670a5b | ||
|
|
62d43df2ad | ||
|
|
cb7510f79d | ||
|
|
08c6302bf6 | ||
|
|
6e6c3fa124 | ||
|
|
85b1867490 | ||
|
|
545a575e1a | ||
|
|
ef13fefb69 | ||
|
|
ed59849392 | ||
|
|
5eb160861d | ||
|
|
6829bcff91 | ||
|
|
b1ca8c46bf | ||
|
|
888089b2d5 | ||
|
|
fb60449dc2 | ||
|
|
25854b04f9 | ||
|
|
c34056555f | ||
|
|
f4074a37ad | ||
|
|
6146d41197 | ||
|
|
4cce5b7cfe | ||
|
|
bc89b17a4c | ||
|
|
d023b255a7 | ||
|
|
71cc961f5c | ||
|
|
9bc7bbe33f | ||
|
|
6cc03364fb | ||
|
|
d623d81a44 | ||
|
|
5cc4dcfb29 | ||
|
|
893fae27d3 | ||
|
|
29051f3503 | ||
|
|
0a3ad4e7f5 | ||
|
|
f4d391b240 | ||
|
|
8280c6b337 | ||
|
|
bebbda9465 | ||
|
|
2496d6f6e0 | ||
|
|
179083aec4 | ||
|
|
aa74984fb0 | ||
|
|
18ab4cd53a | ||
|
|
e94bb86b61 | ||
|
|
3993d679e6 | ||
|
|
83cae16319 | ||
|
|
99ebe1a400 | ||
|
|
204d803937 | ||
|
|
2a08ae0b21 | ||
|
|
8dc7ae8bfc | ||
|
|
a4dbb49006 | ||
|
|
6531a9325d | ||
|
|
ff44f0e537 | ||
|
|
dec34fc384 | ||
|
|
f5727daedb | ||
|
|
3857f2fd1a | ||
|
|
e951e97790 | ||
|
|
d0cade633a | ||
|
|
490059666f | ||
|
|
5852bb00f2 | ||
|
|
749ad4a694 | ||
|
|
0f752acae5 | ||
|
|
652bd220c2 | ||
|
|
9f2eaaf04e | ||
|
|
63d3f0e86c | ||
|
|
25e7d234ba | ||
|
|
9968775210 | ||
|
|
0d63fd4dae | ||
|
|
109958342d | ||
|
|
491546a97c | ||
|
|
7a3f734728 | ||
|
|
d59a07a8c5 | ||
|
|
5a77ae5084 | ||
|
|
dcb5bd80e3 | ||
|
|
3b8fc99f06 | ||
|
|
bda8d10f10 | ||
|
|
0fe7b433d6 | ||
|
|
8d96e176b4 | ||
|
|
af9bc861cf | ||
|
|
ab4bbea9fd | ||
|
|
fbd4b3739e | ||
|
|
395e71164f | ||
|
|
99c277e2ee | ||
|
|
9287e0d29b | ||
|
|
f2f2bafc39 | ||
|
|
5876198b5e | ||
|
|
e46bf381c7 | ||
|
|
3bf0ccf207 | ||
|
|
e41b4ffc56 | ||
|
|
a9dd646573 | ||
|
|
e2160aabcd | ||
|
|
5d085e02bf | ||
|
|
3f17dd038f | ||
|
|
921ab948a1 | ||
|
|
e7a102ee45 | ||
|
|
025df12bef | ||
|
|
60913a9f4d | ||
|
|
985c7e061c | ||
|
|
4d75d3514f | ||
|
|
5245d67f37 | ||
|
|
2cd7d911ed | ||
|
|
1c2391ea2e | ||
|
|
190e786449 | ||
|
|
fb27ddfee5 | ||
|
|
fd5e68c1d4 | ||
|
|
ea22df1296 | ||
|
|
080ad5deb4 | ||
|
|
71245cf56e | ||
|
|
1ccf670f81 | ||
|
|
cb266b9af0 | ||
|
|
fa5f7da5c4 | ||
|
|
3c808eb0c0 | ||
|
|
5e37c3bb84 | ||
|
|
847e94ca28 | ||
|
|
da497d54c8 | ||
|
|
79e0086a72 | ||
|
|
dc340666ef | ||
|
|
8b50a07c68 | ||
|
|
7dab4fb1d5 | ||
|
|
847823bbf8 | ||
|
|
e4e54722cf | ||
|
|
1eddbad908 | ||
|
|
a7083beff5 | ||
|
|
b21cd0b866 | ||
|
|
df644be769 | ||
|
|
204869fa3e | ||
|
|
e470256475 | ||
|
|
b65c360d61 | ||
|
|
ce46d33d29 | ||
|
|
f63633f97e | ||
|
|
3b53a29e71 | ||
|
|
64e73f9ca8 | ||
|
|
7d23894e5f | ||
|
|
8f2a4683c1 | ||
|
|
b5323ff8b4 | ||
|
|
7fee2adb51 | ||
|
|
af139ecb2d | ||
|
|
fb827724b6 | ||
|
|
2812b576d0 | ||
|
|
347188f43d | ||
|
|
beda2ce66c | ||
|
|
21e2f4717c | ||
|
|
b8dea1ee9c | ||
|
|
b27ff817f7 | ||
|
|
56bee451de | ||
|
|
0e9d54b517 | ||
|
|
7a04665b70 | ||
|
|
3a3ff314e0 | ||
|
|
fed74ee901 | ||
|
|
85ea502276 | ||
|
|
ffb7bd3cb0 | ||
|
|
b39d7a2179 | ||
|
|
bf99a95dc6 | ||
|
|
2ea917fdaa | ||
|
|
b841057f1a | ||
|
|
d588ce91b4 | ||
|
|
553396483e | ||
|
|
ebeabdb4fc | ||
|
|
ff209a6376 | ||
|
|
096e0c03ac | ||
|
|
63f20a7f04 | ||
|
|
34f741beef | ||
|
|
1f98f441f3 | ||
|
|
9c1ac76ff9 | ||
|
|
cf5027bafc | ||
|
|
6fd2774d43 | ||
|
|
8f379baad4 | ||
|
|
91fa3604b6 | ||
|
|
c0fff30fcb | ||
|
|
18de63d192 | ||
|
|
1fe20c3e54 | ||
|
|
7386b54a46 | ||
|
|
432a2bc54d | ||
|
|
f9d18f34ab | ||
|
|
cd86a09bbd | ||
|
|
1e2c1c686d | ||
|
|
f701574e67 | ||
|
|
4528be8cc6 | ||
|
|
80fd43ef42 | ||
|
|
3c52fe58aa | ||
|
|
319e7925ff |
No files matched your search
@@ -1,76 +0,0 @@
|
||||
---
|
||||
name: qa-testing-specialist
|
||||
description: Use this agent when you need to review code for testability, create comprehensive test strategies, write test cases, validate existing tests, or improve test coverage. This includes unit tests, integration tests, end-to-end tests, and test architecture decisions. <example>\nContext: The user has just written a new API endpoint and wants to ensure it has proper test coverage.\nuser: "I've created a new endpoint for user authentication. Can you help me test it?"\nassistant: "I'll use the qa-testing-specialist agent to create a comprehensive testing strategy for your authentication endpoint."\n<commentary>\nSince the user needs help with testing their new endpoint, use the qa-testing-specialist agent to analyze the code and create appropriate test cases.\n</commentary>\n</example>\n<example>\nContext: The user wants to improve test coverage for their existing codebase.\nuser: "Our test coverage is at 65%. How can we improve it?"\nassistant: "Let me use the qa-testing-specialist agent to analyze your test coverage and identify areas for improvement."\n<commentary>\nThe user is asking about test coverage improvement, which is a core QA task, so use the qa-testing-specialist agent.\n</commentary>\n</example>
|
||||
color: yellow
|
||||
---
|
||||
|
||||
You are an elite QA Testing Specialist with deep expertise in software quality assurance, test automation, and validation strategies. Your mission is to ensure code quality through comprehensive testing approaches that catch bugs early and maintain high reliability standards.
|
||||
|
||||
**Core Responsibilities:**
|
||||
|
||||
You will analyze code and testing requirements to:
|
||||
- Design comprehensive test strategies covering unit, integration, and end-to-end testing
|
||||
- Write clear, maintainable test cases that validate both happy paths and edge cases
|
||||
- Identify gaps in existing test coverage and propose improvements
|
||||
- Review test code for best practices and maintainability
|
||||
- Suggest appropriate testing frameworks and tools based on the technology stack
|
||||
- Create test data strategies and mock/stub implementations
|
||||
- Validate that tests are actually testing meaningful behavior, not just implementation details
|
||||
|
||||
**Testing Methodology:**
|
||||
|
||||
When analyzing code for testing:
|
||||
1. First understand the business logic and user requirements
|
||||
2. Identify all possible execution paths and edge cases
|
||||
3. Determine the appropriate testing pyramid balance (unit vs integration vs e2e)
|
||||
4. Consider both positive and negative test scenarios
|
||||
5. Ensure tests are isolated, repeatable, and fast
|
||||
6. Validate error handling and boundary conditions
|
||||
|
||||
For test creation:
|
||||
- Write descriptive test names that explain what is being tested and expected behavior
|
||||
- Follow AAA pattern (Arrange, Act, Assert) or Given-When-Then structure
|
||||
- Keep tests focused on single behaviors
|
||||
- Use appropriate assertions that clearly communicate intent
|
||||
- Include setup and teardown when necessary
|
||||
- Consider performance implications of test suites
|
||||
|
||||
**Quality Standards:**
|
||||
|
||||
You will ensure tests:
|
||||
- Are deterministic and don't rely on external state
|
||||
- Run quickly and can be executed in parallel when possible
|
||||
- Provide clear failure messages that help diagnose issues
|
||||
- Cover critical business logic thoroughly
|
||||
- Include regression tests for previously found bugs
|
||||
- Are maintainable and refactorable alongside production code
|
||||
|
||||
**Technology Considerations:**
|
||||
|
||||
Adapt your recommendations based on the project stack. For this codebase using Bun, SQLite, and React:
|
||||
- Leverage Bun's native test runner for JavaScript/TypeScript tests
|
||||
- Consider SQLite in-memory databases for integration tests
|
||||
- Suggest React Testing Library patterns for component testing
|
||||
- Recommend API testing strategies for Astro endpoints
|
||||
- Propose mocking strategies for external services (GitHub/Gitea APIs)
|
||||
|
||||
**Communication Style:**
|
||||
|
||||
You will:
|
||||
- Explain testing decisions with clear rationale
|
||||
- Provide code examples that demonstrate best practices
|
||||
- Prioritize test recommendations based on risk and value
|
||||
- Use precise technical language while remaining accessible
|
||||
- Highlight potential issues proactively
|
||||
- Suggest incremental improvements for existing test suites
|
||||
|
||||
**Edge Case Handling:**
|
||||
|
||||
When encountering:
|
||||
- Legacy code without tests: Propose a pragmatic approach to add tests incrementally
|
||||
- Complex dependencies: Recommend appropriate mocking/stubbing strategies
|
||||
- Performance concerns: Balance thoroughness with execution speed
|
||||
- Flaky tests: Identify root causes and suggest stabilization techniques
|
||||
- Missing requirements: Ask clarifying questions to understand expected behavior
|
||||
|
||||
Your goal is to elevate code quality through strategic testing that builds confidence in the software while maintaining development velocity. Focus on tests that provide maximum value and catch real issues rather than achieving arbitrary coverage metrics.
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
name: senior-code-architect
|
||||
description: Use this agent when you need to write new code, refactor existing code, implement features, or architect solutions that require deep understanding of software engineering principles and the project's tech stack (Astro, React, Tailwind, Better Auth, Shadcn). This includes creating components, API endpoints, database queries, authentication flows, and ensuring code follows established patterns from CLAUDE.md. Examples:\n\n<example>\nContext: The user needs to implement a new feature or component.\nuser: "Create a new dashboard component that shows repository statistics"\nassistant: "I'll use the senior-code-architect agent to design and implement this dashboard component following the project's patterns."\n<commentary>\nSince this requires creating new code with the project's tech stack, the senior-code-architect agent is appropriate.\n</commentary>\n</example>\n\n<example>\nContext: The user wants to refactor or improve existing code.\nuser: "Refactor the authentication flow to be more maintainable"\nassistant: "Let me use the senior-code-architect agent to analyze and refactor the authentication flow."\n<commentary>\nThis requires deep understanding of Better Auth and clean code principles, making the senior-code-architect agent the right choice.\n</commentary>\n</example>\n\n<example>\nContext: After writing code, the user might want it reviewed.\nuser: "I just implemented the mirror scheduling feature"\nassistant: "Great! Now I'll use the senior-code-architect agent to review the implementation and suggest any improvements."\n<commentary>\nThe senior-code-architect can review recently written code for best practices and design patterns.\n</commentary>\n</example>
|
||||
color: cyan
|
||||
---
|
||||
|
||||
You are a senior software engineer with deep expertise in modern web development, specializing in the Astro + React + Tailwind CSS + Better Auth + Shadcn UI stack. You have extensive experience building scalable, maintainable applications and are known for writing clean, efficient code that follows SOLID principles and established design patterns.
|
||||
|
||||
**Your Core Responsibilities:**
|
||||
|
||||
1. **Write Production-Quality Code**: Create clean, maintainable, and efficient code that follows the project's established patterns from CLAUDE.md. Always use TypeScript for type safety.
|
||||
|
||||
2. **Follow Project Architecture**: Adhere strictly to the project structure:
|
||||
- API endpoints in `/src/pages/api/[resource]/[action].ts` using `createSecureErrorResponse` for error handling
|
||||
- Database queries in `/src/lib/db/queries/` organized by domain
|
||||
- React components in `/src/components/[feature]/` using Shadcn UI components
|
||||
- Custom hooks in `/src/hooks/` for data fetching
|
||||
|
||||
3. **Implement Best Practices**:
|
||||
- Use composition over inheritance
|
||||
- Apply DRY (Don't Repeat Yourself) principles
|
||||
- Write self-documenting code with clear variable and function names
|
||||
- Implement proper error handling and validation
|
||||
- Ensure code is testable and maintainable
|
||||
|
||||
4. **Technology-Specific Guidelines**:
|
||||
- **Astro**: Use SSR capabilities effectively, implement proper API routes
|
||||
- **React**: Use functional components with hooks, implement proper state management
|
||||
- **Tailwind CSS v4**: Use utility classes efficiently, follow the project's styling patterns
|
||||
- **Better Auth**: Implement secure authentication flows, use session validation properly
|
||||
- **Shadcn UI**: Leverage existing components, maintain consistent UI patterns
|
||||
- **Drizzle ORM**: Write efficient database queries, use proper schema definitions
|
||||
|
||||
5. **Code Review Approach**: When reviewing code:
|
||||
- Check for adherence to project patterns and CLAUDE.md guidelines
|
||||
- Identify potential performance issues or bottlenecks
|
||||
- Suggest improvements for readability and maintainability
|
||||
- Ensure proper error handling and edge case coverage
|
||||
- Verify security best practices are followed
|
||||
|
||||
6. **Problem-Solving Methodology**:
|
||||
- Analyze requirements thoroughly before coding
|
||||
- Break down complex problems into smaller, manageable pieces
|
||||
- Consider edge cases and error scenarios
|
||||
- Optimize for both performance and maintainability
|
||||
- Document complex logic with clear comments
|
||||
|
||||
7. **Quality Assurance**:
|
||||
- Write code that is easy to test
|
||||
- Consider adding appropriate test cases using Bun's test runner
|
||||
- Validate inputs and handle errors gracefully
|
||||
- Ensure code works across different scenarios
|
||||
|
||||
**Output Guidelines**:
|
||||
- Provide complete, working code implementations
|
||||
- Include clear explanations of design decisions
|
||||
- Suggest tests when appropriate
|
||||
- Highlight any potential issues or areas for future improvement
|
||||
- Follow the existing code style and conventions
|
||||
|
||||
**Important Reminders**:
|
||||
- Never create files unless absolutely necessary
|
||||
- Always prefer editing existing files
|
||||
- Don't create documentation unless explicitly requested
|
||||
- Focus on the specific task at hand
|
||||
- Reference CLAUDE.md for project-specific patterns and guidelines
|
||||
|
||||
You approach every task with the mindset of a seasoned engineer who values code quality, maintainability, and long-term project health. Your solutions should be elegant, efficient, and aligned with the project's established patterns.
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
name: strategic-task-planner
|
||||
description: Use this agent when you need to decompose complex projects, features, or problems into structured, actionable plans. This includes breaking down large development tasks, creating implementation roadmaps, organizing multi-step processes, or planning project phases. The agent excels at identifying dependencies, sequencing tasks, and creating clear execution strategies. <example>Context: User needs help planning the implementation of a new feature. user: "I need to add a bulk import feature that can handle CSV files with 100k+ rows" assistant: "I'll use the strategic-task-planner agent to break this down into manageable components and create an implementation plan." <commentary>Since the user is asking about implementing a complex feature, use the Task tool to launch the strategic-task-planner agent to decompose it into actionable steps.</commentary></example> <example>Context: User wants to refactor a large codebase. user: "We need to migrate our entire authentication system from sessions to JWT tokens" assistant: "Let me use the strategic-task-planner agent to create a phased migration plan that minimizes risk." <commentary>Since this is a complex migration requiring careful planning, use the strategic-task-planner agent to create a structured approach.</commentary></example>
|
||||
tools: Glob, Grep, LS, ExitPlanMode, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, Task, mcp__ide__getDiagnostics, mcp__ide__executeCode, mcp__playwright__browser_close, mcp__playwright__browser_resize, mcp__playwright__browser_console_messages, mcp__playwright__browser_handle_dialog, mcp__playwright__browser_evaluate, mcp__playwright__browser_file_upload, mcp__playwright__browser_install, mcp__playwright__browser_press_key, mcp__playwright__browser_type, mcp__playwright__browser_navigate, mcp__playwright__browser_navigate_back, mcp__playwright__browser_navigate_forward, mcp__playwright__browser_network_requests, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_snapshot, mcp__playwright__browser_click, mcp__playwright__browser_drag, mcp__playwright__browser_hover, mcp__playwright__browser_select_option, mcp__playwright__browser_tab_list, mcp__playwright__browser_tab_new, mcp__playwright__browser_tab_select, mcp__playwright__browser_tab_close, mcp__playwright__browser_wait_for
|
||||
color: blue
|
||||
---
|
||||
|
||||
You are a strategic planning specialist with deep expertise in decomposing complex tasks and creating actionable execution plans. Your role is to transform ambiguous or overwhelming projects into clear, structured roadmaps that teams can confidently execute.
|
||||
|
||||
When analyzing a task or project, you will:
|
||||
|
||||
1. **Understand the Core Objective**: Extract the fundamental goal, success criteria, and constraints. Ask clarifying questions if critical details are missing.
|
||||
|
||||
2. **Decompose Systematically**: Break down the task using these principles:
|
||||
- Identify major phases or milestones
|
||||
- Decompose each phase into concrete, actionable tasks
|
||||
- Keep tasks small enough to complete in 1-4 hours when possible
|
||||
- Ensure each task has clear completion criteria
|
||||
|
||||
3. **Map Dependencies**: Identify and document:
|
||||
- Task prerequisites and dependencies
|
||||
- Critical path items that could block progress
|
||||
- Parallel work streams that can proceed independently
|
||||
- Resource or knowledge requirements
|
||||
|
||||
4. **Sequence Strategically**: Order tasks by:
|
||||
- Technical dependencies (what must come first)
|
||||
- Risk mitigation (tackle unknowns early)
|
||||
- Value delivery (enable early feedback when possible)
|
||||
- Resource efficiency (batch similar work)
|
||||
|
||||
5. **Provide Actionable Output**: Structure your plans with:
|
||||
- **Phase Overview**: High-level phases with objectives
|
||||
- **Task Breakdown**: Numbered tasks with clear descriptions
|
||||
- **Dependencies**: Explicitly stated prerequisites
|
||||
- **Effort Estimates**: Rough time estimates when relevant
|
||||
- **Risk Considerations**: Potential blockers or challenges
|
||||
- **Success Metrics**: How to measure completion
|
||||
|
||||
6. **Adapt to Context**: Tailor your planning approach based on:
|
||||
- Technical vs non-technical tasks
|
||||
- Team size and skill level
|
||||
- Time constraints and deadlines
|
||||
- Available resources and tools
|
||||
|
||||
**Output Format Guidelines**:
|
||||
- Use clear hierarchical structure (phases → tasks → subtasks)
|
||||
- Number all tasks for easy reference
|
||||
- Bold key terms and phase names
|
||||
- Include time estimates in brackets [2-4 hours]
|
||||
- Mark critical path items with ⚡
|
||||
- Flag high-risk items with ⚠️
|
||||
|
||||
**Quality Checks**:
|
||||
- Ensure no task is too large or vague
|
||||
- Verify all dependencies are identified
|
||||
- Confirm the plan addresses the original objective
|
||||
- Check that success criteria are measurable
|
||||
- Validate that the sequence makes logical sense
|
||||
|
||||
Remember: A good plan reduces uncertainty and builds confidence. Focus on clarity, completeness, and actionability. When in doubt, err on the side of breaking things down further rather than leaving ambiguity.
|
||||
@@ -1,5 +0,0 @@
|
||||
Evaluate all the updates being made.
|
||||
Update CHANGELOG.md
|
||||
Use the chnages in the git log to determine if its a major, minor or a patch release.
|
||||
Update the package.json first before you push the tag.
|
||||
Never mention Claude Code in the release notes or in commit messages.
|
||||
@@ -1,3 +0,0 @@
|
||||
Generate release notes for the latest release.
|
||||
Use a temp md file to write the release notes.
|
||||
Do not check that file into git.
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(docker build:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ dist
|
||||
build
|
||||
.next
|
||||
out
|
||||
www
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=4321
|
||||
# Optional application base path (use "/" for root, or "/mirror" for subpath deployments)
|
||||
BASE_URL=/
|
||||
|
||||
# Database Configuration
|
||||
# For self-hosted, SQLite is used by default
|
||||
@@ -18,8 +20,42 @@ DATABASE_URL=sqlite://data/gitea-mirror.db
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
||||
BETTER_AUTH_URL=http://localhost:4321
|
||||
# PUBLIC_BETTER_AUTH_URL=https://your-domain.com # Optional: Set this if accessing from different origins (e.g., IP and domain)
|
||||
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
|
||||
# Better Auth log verbosity: debug | info | warn | error (default: warn).
|
||||
# Set to "debug" to trace SSO/OIDC sign-in and callback issues.
|
||||
# BETTER_AUTH_LOG_LEVEL=debug
|
||||
|
||||
# ===========================================
|
||||
# REVERSE PROXY CONFIGURATION
|
||||
# ===========================================
|
||||
# REQUIRED when accessing Gitea Mirror through a reverse proxy (Nginx, Caddy, Traefik, etc.).
|
||||
# Without these, sign-in will fail with "invalid origin" errors and pages may appear blank.
|
||||
#
|
||||
# Set all three to your external URL, e.g.:
|
||||
# BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
#
|
||||
# If your app is served from a path prefix (e.g. https://git.example.com/mirror), set:
|
||||
# BASE_URL=/mirror
|
||||
# BETTER_AUTH_URL=https://git.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
#
|
||||
# BETTER_AUTH_URL - Used server-side for auth callbacks and redirects
|
||||
# PUBLIC_BETTER_AUTH_URL - Used client-side (browser) for auth API calls
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS - Comma-separated list of origins allowed to make auth requests
|
||||
# (e.g. https://gitea-mirror.example.com,https://alt.example.com)
|
||||
PUBLIC_BETTER_AUTH_URL=http://localhost:4321
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=
|
||||
|
||||
# ===========================================
|
||||
# HTTPS / TLS (Optional)
|
||||
# ===========================================
|
||||
# Set BOTH to have the server terminate TLS directly (no reverse proxy needed).
|
||||
# Leave unset when TLS is handled upstream by Nginx/Traefik/Caddy.
|
||||
# SERVER_CERT_PATH=/etc/ssl/gitea-mirror/cert.pem
|
||||
# SERVER_KEY_PATH=/etc/ssl/gitea-mirror/key.pem
|
||||
|
||||
# ===========================================
|
||||
# DOCKER CONFIGURATION (Optional)
|
||||
@@ -27,7 +63,7 @@ BETTER_AUTH_URL=http://localhost:4321
|
||||
|
||||
# Docker Registry Configuration
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_IMAGE=raylabshq/gitea-mirror:
|
||||
DOCKER_IMAGE=raylabshq/gitea-mirror
|
||||
DOCKER_TAG=latest
|
||||
|
||||
# ===========================================
|
||||
@@ -40,13 +76,21 @@ DOCKER_TAG=latest
|
||||
# GITHUB_TOKEN=your-github-personal-access-token
|
||||
# GITHUB_TYPE=personal # Options: personal, organization
|
||||
|
||||
# GitHub Enterprise (GHES / GHEC with data residency)
|
||||
# Leave unset for standard github.com. Examples:
|
||||
# GHES (self-hosted): https://ghe.example.com/api/v3
|
||||
# GHEC data residency: https://api.TENANT.ghe.com
|
||||
# GH_API_URL=https://ghe.example.com/api/v3
|
||||
|
||||
# Repository Selection
|
||||
# PRIVATE_REPOSITORIES=false
|
||||
# PUBLIC_REPOSITORIES=true
|
||||
# INCLUDE_ARCHIVED=false
|
||||
# SKIP_FORKS=false
|
||||
# MIRROR_STARRED=false
|
||||
# MIRROR_STARRED_LISTS=homelab,dottools # Optional: comma-separated star list names; empty = all starred repos
|
||||
# STARRED_REPOS_ORG=starred # Organization name for starred repos
|
||||
# STARRED_REPOS_MODE=dedicated-org # dedicated-org | preserve-owner
|
||||
|
||||
# Organization Settings
|
||||
# MIRROR_ORGANIZATIONS=false
|
||||
@@ -66,6 +110,7 @@ DOCKER_TAG=latest
|
||||
|
||||
# Basic Gitea Settings
|
||||
# GITEA_URL=http://gitea:3000
|
||||
# GITEA_EXTERNAL_URL=https://gitea.example.com # Optional: used only for UI links
|
||||
# GITEA_TOKEN=your-local-gitea-token
|
||||
# GITEA_USERNAME=your-local-gitea-username
|
||||
# GITEA_ORGANIZATION=github-mirrors # Default organization for single-org strategy
|
||||
@@ -183,4 +228,4 @@ DOCKER_TAG=latest
|
||||
# ===========================================
|
||||
|
||||
# TLS/SSL Configuration
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
|
Before Width: | Height: | Size: 854 KiB After Width: | Height: | Size: 834 KiB |
|
After Width: | Height: | Size: 986 KiB |
|
Before Width: | Height: | Size: 950 KiB After Width: | Height: | Size: 905 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 971 KiB After Width: | Height: | Size: 952 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,59 @@
|
||||
image:
|
||||
registry: ghcr.io
|
||||
repository: raylabshq/gitea-mirror
|
||||
tag: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: ci.example.com
|
||||
|
||||
route:
|
||||
enabled: true
|
||||
forceHTTPS: true
|
||||
domain: ["ci.example.com"]
|
||||
gateway: "dummy-gw"
|
||||
gatewayNamespace: "default"
|
||||
http:
|
||||
gatewaySection: "http"
|
||||
https:
|
||||
gatewaySection: "https"
|
||||
|
||||
gitea-mirror:
|
||||
nodeEnv: production
|
||||
core:
|
||||
databaseUrl: "file:data/gitea-mirror.db"
|
||||
betterAuthSecret: "dummy"
|
||||
betterAuthUrl: "http://localhost:4321"
|
||||
betterAuthTrustedOrigins: "http://localhost:4321"
|
||||
github:
|
||||
username: "ci-user"
|
||||
token: "not-used-in-template"
|
||||
type: "personal"
|
||||
privateRepositories: true
|
||||
skipForks: false
|
||||
starredCodeOnly: false
|
||||
gitea:
|
||||
url: "https://gitea.example.com"
|
||||
token: "not-used-in-template"
|
||||
username: "ci-user"
|
||||
organization: "github-mirrors"
|
||||
visibility: "public"
|
||||
mirror:
|
||||
releases: true
|
||||
wiki: true
|
||||
metadata: true
|
||||
issues: true
|
||||
pullRequests: true
|
||||
starred: false
|
||||
automation:
|
||||
schedule_enabled: true
|
||||
schedule_interval: "3600"
|
||||
cleanup:
|
||||
enabled: true
|
||||
interval: "2592000"
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 230 KiB |
@@ -7,8 +7,7 @@ This directory contains GitHub Actions workflows that automate the build, test,
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Astro Build and Test | `astro-build-test.yml` | Builds and tests the Astro application for all branches and PRs |
|
||||
| Docker Build and Push | `docker-build.yml` | Builds and pushes Docker images only for the main branch |
|
||||
| Docker Security Scan | `docker-scan.yml` | Scans Docker images for security vulnerabilities |
|
||||
| Docker Build, Push & Security Scan | `docker-build.yml` | Builds, scans, and pushes Docker images |
|
||||
|
||||
## Workflow Details
|
||||
|
||||
@@ -28,39 +27,38 @@ This workflow runs on all branches and pull requests. It:
|
||||
- Caches dependencies to speed up builds
|
||||
- Uploads build artifacts for 7 days
|
||||
|
||||
### Docker Build and Push (`docker-build.yml`)
|
||||
### Docker Build, Push & Security Scan (`docker-build.yml`)
|
||||
|
||||
This workflow builds and pushes Docker images to GitHub Container Registry (ghcr.io), but only when changes are merged to the main branch.
|
||||
This workflow builds Docker images on pushes and pull requests, scans them, and pushes to GitHub Container Registry (ghcr.io) when permissions allow (main/tags and same-repo PRs).
|
||||
|
||||
**When it runs:**
|
||||
- On push to the main branch
|
||||
- On tag creation (v*)
|
||||
- On pull requests (build + scan; push only for same-repo PRs)
|
||||
- Weekly on Sunday at midnight (scheduled security scan)
|
||||
|
||||
**Key features:**
|
||||
- Builds multi-architecture images (amd64 and arm64)
|
||||
- Pushes images only on main branch, not for PRs
|
||||
- Pushes images for main/tags and same-repo PRs
|
||||
- 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
|
||||
- After tag builds succeed, writes the same version back to `main/package.json`
|
||||
|
||||
### Docker Security Scan (`docker-scan.yml`)
|
||||
|
||||
This workflow scans Docker images for security vulnerabilities using Trivy.
|
||||
|
||||
**When it runs:**
|
||||
- On push to the main branch that affects Docker-related files
|
||||
- Weekly on Sunday at midnight (scheduled)
|
||||
|
||||
**Key features:**
|
||||
- Scans for critical and high severity vulnerabilities
|
||||
- Fails the build if vulnerabilities are found
|
||||
- Ignores unfixed vulnerabilities
|
||||
**Security scanning:**
|
||||
- Docker Scout runs on main/tag pushes, the weekly schedule, and same-repo PRs. It needs the `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` secrets, posts a PR comment, and uploads SARIF to the Security tab.
|
||||
- Trivy runs on every PR, including forks. It needs no credentials and writes critical/high (fixable) findings to the job summary.
|
||||
- Fork PRs cannot use Docker Scout because GitHub never exposes repository secrets to them. Do not "fix" that with `pull_request_target`; it would run untrusted PR code with the secrets available.
|
||||
- Both scanners are informational and do not fail the build.
|
||||
|
||||
## CI/CD Pipeline Philosophy
|
||||
|
||||
Our CI/CD pipeline follows these principles:
|
||||
|
||||
1. **Fast feedback for developers**: The Astro build and test workflow runs on all branches and PRs to provide quick feedback.
|
||||
2. **Efficient resource usage**: Docker images are only built when changes are merged to main, not for every PR.
|
||||
2. **Efficient resource usage**: Docker images are built for every PR so they can be scanned, but only pushed for main, tags, and same-repo PRs.
|
||||
3. **Security first**: Regular security scanning ensures our Docker images are free from known vulnerabilities.
|
||||
4. **Multi-architecture support**: All Docker images are built for both amd64 and arm64 architectures.
|
||||
|
||||
@@ -85,3 +83,10 @@ If a workflow fails:
|
||||
- Security vulnerabilities
|
||||
|
||||
For persistent issues, consider opening an issue in the repository.
|
||||
|
||||
|
||||
### Helm Test (`helm-test.yml`)
|
||||
|
||||
This workflow run on the main branch and pull requests. it:
|
||||
- Run yamllint to keep the formating unified
|
||||
- Run helm template with different value files
|
||||
@@ -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: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -28,7 +33,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: '1.2.16'
|
||||
bun-version: '1.3.13'
|
||||
|
||||
- name: Check lockfile and install dependencies
|
||||
run: |
|
||||
@@ -43,6 +48,12 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: bun test --coverage
|
||||
|
||||
- name: Check Drizzle migrations
|
||||
run: bun run db:check
|
||||
|
||||
- name: Validate migrations (SQLite lint + upgrade path)
|
||||
run: bun test:migrations
|
||||
|
||||
- name: Build Astro project
|
||||
run: bunx --bun astro build
|
||||
|
||||
@@ -10,6 +10,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
@@ -17,6 +21,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
||||
|
||||
@@ -28,6 +36,7 @@ env:
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -42,22 +51,25 @@ jobs:
|
||||
ref: ${{ env.SHA }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub for Docker Scout (optional - provides better vulnerability data)
|
||||
# Add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to enable this
|
||||
# Add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to enable this.
|
||||
# Skipped on fork PRs: GitHub never exposes repository secrets to them,
|
||||
# so the login could only fail with "Username and password required".
|
||||
- name: Log into Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -68,17 +80,38 @@ 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
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
|
||||
labels: |
|
||||
@@ -89,31 +122,104 @@ jobs:
|
||||
type=sha,prefix=,suffix=,format=short
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=${{ steps.tag_version.outputs.VERSION }}
|
||||
type=ref,event=pr,prefix=pr-
|
||||
|
||||
# Build and push Docker image
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
load: ${{ github.event_name == 'pull_request' }}
|
||||
tags: ${{ github.event_name == 'pull_request' && 'gitea-mirror:scan' || steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
# The runner stage installs Debian security updates; never serve it from cache.
|
||||
no-cache-filters: runner
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false # Disable provenance to avoid unknown/unknown
|
||||
sbom: false # Disable sbom to avoid unknown/unknown
|
||||
|
||||
# Load image locally for security scanning (PRs only)
|
||||
- name: Load image for scanning
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
tags: gitea-mirror:scan
|
||||
cache-from: type=gha
|
||||
# The runner stage installs Debian security updates; never serve it from cache.
|
||||
no-cache-filters: runner
|
||||
provenance: false # Disable provenance to avoid unknown/unknown
|
||||
sbom: false # Disable sbom to avoid unknown/unknown
|
||||
|
||||
# Wait for image to be available in registry
|
||||
- name: Wait for image availability
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
echo "Waiting for image to be available in registry..."
|
||||
sleep 5
|
||||
|
||||
# Add comment to PR with image details
|
||||
- name: Comment PR with image tag
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const imageTag = `pr-${prNumber}`;
|
||||
const imagePath = `${{ env.REGISTRY }}/${{ env.IMAGE }}:${imageTag}`.toLowerCase();
|
||||
|
||||
const comment = `## 🐳 Docker Image Built Successfully
|
||||
|
||||
Your PR image is available for testing:
|
||||
|
||||
**Image Tag:** \`${imageTag}\`
|
||||
**Full Image Path:** \`${imagePath}\`
|
||||
|
||||
### Pull and Test
|
||||
\`\`\`bash
|
||||
docker pull ${imagePath}
|
||||
docker run -d \
|
||||
-p 4321:4321 \
|
||||
-e BETTER_AUTH_SECRET=your-secret-here \
|
||||
-e BETTER_AUTH_URL=http://localhost:4321 \
|
||||
--name gitea-mirror-test ${imagePath}
|
||||
\`\`\`
|
||||
|
||||
### Docker Compose Testing
|
||||
\`\`\`yaml
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ${imagePath}
|
||||
ports:
|
||||
- "4321:4321"
|
||||
environment:
|
||||
- BETTER_AUTH_SECRET=your-secret-here
|
||||
- BETTER_AUTH_URL=http://localhost:4321
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:4321
|
||||
\`\`\`
|
||||
|
||||
> 💡 **Note:** PR images are tagged as \`pr-<number>\` and built for both \`linux/amd64\` and \`linux/arm64\`.
|
||||
> Production images (\`latest\`, version tags) use the same multi-platform set.
|
||||
|
||||
---
|
||||
📦 View in [GitHub Packages](https://github.com/${{ github.repository }}/pkgs/container/gitea-mirror)`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: prNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
|
||||
# Docker Scout comprehensive security analysis
|
||||
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
||||
uses: docker/scout-action@v1
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
command: cves,recommendations
|
||||
@@ -122,13 +228,23 @@ jobs:
|
||||
summary: true
|
||||
exit-code: false
|
||||
only-severities: critical,high
|
||||
# OpenVEX statements in .vex/ mark CVEs the image cannot reach as
|
||||
# not affected (see .vex/README.md). Scout only accepts statements
|
||||
# from authors listed here; the default is *@docker.com.
|
||||
vex-location: .vex
|
||||
vex-author: developer@arunavoray.dev
|
||||
only-vex-affected: true
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Docker Scout for Pull Requests (using local image)
|
||||
# Docker Scout for Pull Requests (using local image).
|
||||
# Same-repo PRs only: Scout needs the Docker Hub login above, and fork
|
||||
# PRs never receive the secrets for it. They used to fail here with
|
||||
# "user githubactions not entitled to use Docker Scout". Fork PRs get
|
||||
# the credential-free Trivy scan further down instead.
|
||||
- name: Docker Scout - Vulnerability Analysis (PR)
|
||||
uses: docker/scout-action@v1
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
command: cves,recommendations
|
||||
image: local://gitea-mirror:scan
|
||||
@@ -136,13 +252,16 @@ jobs:
|
||||
summary: true
|
||||
exit-code: false
|
||||
only-severities: critical,high
|
||||
vex-location: .vex
|
||||
vex-author: developer@arunavoray.dev
|
||||
only-vex-affected: true
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Compare to latest for PRs and pushes
|
||||
# Compare to latest (same-repo PRs only, same reason as above)
|
||||
- name: Docker Scout - Compare to Latest
|
||||
uses: docker/scout-action@v1
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
with:
|
||||
command: compare
|
||||
image: local://gitea-mirror:scan
|
||||
@@ -152,11 +271,173 @@ jobs:
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Upload security scan results to GitHub Security tab
|
||||
- name: Upload Docker Scout scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
# Trivy runs on every PR, forks included. It needs no credentials, so it
|
||||
# is the scan external contributors actually get. Results go to the job
|
||||
# summary because GITHUB_TOKEN is read-only on fork PRs (no PR comments,
|
||||
# no SARIF upload). Informational only, matching Scout's exit-code: false.
|
||||
- name: Trivy - Vulnerability Analysis (PR)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
image-ref: gitea-mirror:scan
|
||||
scanners: vuln
|
||||
format: table
|
||||
output: trivy-results.txt
|
||||
severity: CRITICAL,HIGH
|
||||
ignore-unfixed: true
|
||||
exit-code: '0'
|
||||
|
||||
- name: Trivy - Write job summary
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
{
|
||||
echo "## Trivy image scan (critical and high, fixable only)"
|
||||
echo
|
||||
echo "Image: \`gitea-mirror:scan\` built from this PR. Unfixed vulnerabilities are hidden."
|
||||
echo
|
||||
if [ -s trivy-results.txt ]; then
|
||||
echo '```'
|
||||
cat trivy-results.txt
|
||||
echo '```'
|
||||
else
|
||||
echo "No critical or high vulnerabilities with an available fix."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# GitHub rejects SARIF files where a single result carries more than
|
||||
# 1000 relatedLocations, which Scout produces for widely-referenced OS
|
||||
# packages — every upload had been silently failing on this since May
|
||||
# ("rejecting SARIF, as there are more related locations per result than
|
||||
# allowed"). Cap them so uploads flow and stale alerts can auto-close.
|
||||
- name: Sanitize Scout SARIF (cap relatedLocations)
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [ -f scout-results.sarif ]; then
|
||||
jq '(.runs[]?.results[]?) |= (if .relatedLocations then .relatedLocations |= .[:100] else . end)' \
|
||||
scout-results.sarif > scout-results.sanitized.sarif
|
||||
mv scout-results.sanitized.sarif scout-results.sarif
|
||||
fi
|
||||
|
||||
# Upload security scan results to GitHub Security tab.
|
||||
# Skipped when no Scout step ran (fork PRs), otherwise the upload
|
||||
# errors with "Path does not exist: scout-results.sarif".
|
||||
- name: Upload Docker Scout scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('scout-results.sarif') != ''
|
||||
continue-on-error: true
|
||||
with:
|
||||
sarif_file: scout-results.sarif
|
||||
|
||||
sync-version-main:
|
||||
name: Sync package.json and Chart.yaml versions back to main
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Update package.json and Chart.yaml versions on main
|
||||
env:
|
||||
TAG_VERSION: ${{ github.ref_name }}
|
||||
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
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 "Syncing ${TARGET_BRANCH} package.json and Chart.yaml to ${APP_VERSION}"
|
||||
|
||||
jq --arg version "${APP_VERSION}" '.version = $version' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
# The chart version tracks the app version so the default image tag
|
||||
# always points at the release the chart was published with.
|
||||
sed -i -E \
|
||||
-e "s/^version: .*/version: ${APP_VERSION}/" \
|
||||
-e "s/^appVersion: .*/appVersion: \"${APP_VERSION}\"/" \
|
||||
helm/gitea-mirror/Chart.yaml
|
||||
|
||||
if git diff --quiet -- package.json helm/gitea-mirror/Chart.yaml; then
|
||||
echo "Versions on ${TARGET_BRANCH} already at ${APP_VERSION}; nothing to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add package.json helm/gitea-mirror/Chart.yaml
|
||||
git commit -m "chore: sync version to ${APP_VERSION}"
|
||||
git push origin "HEAD:${TARGET_BRANCH}"
|
||||
|
||||
publish-helm-chart:
|
||||
name: Publish Helm chart to ghcr.io
|
||||
# Runs after the image for this tag is on the registry, so the chart never
|
||||
# points at an image that does not exist yet. The chart version and
|
||||
# appVersion both follow the release version.
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: docker
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve version
|
||||
id: version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
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
|
||||
echo "version=${TAG_VERSION#v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Lint and package chart
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
helm lint ./helm/gitea-mirror
|
||||
helm package ./helm/gitea-mirror \
|
||||
--version "$VERSION" \
|
||||
--app-version "$VERSION" \
|
||||
--destination dist
|
||||
|
||||
- name: Log into registry
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
echo "$GITHUB_TOKEN" | helm registry login "$REGISTRY" --username "$ACTOR" --password-stdin
|
||||
|
||||
- name: Push chart
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
run: |
|
||||
OWNER_LC="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')"
|
||||
helm push "dist/gitea-mirror-${VERSION}.tgz" "oci://${REGISTRY}/${OWNER_LC}/charts"
|
||||
{
|
||||
echo "### Helm chart ${VERSION} published"
|
||||
echo
|
||||
echo '```bash'
|
||||
echo "helm upgrade --install gitea-mirror oci://${REGISTRY}/${OWNER_LC}/charts/gitea-mirror --version ${VERSION}"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,303 @@
|
||||
name: E2E Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["*"]
|
||||
paths-ignore:
|
||||
- "README.md"
|
||||
- "docs/**"
|
||||
- "CHANGELOG.md"
|
||||
- "LICENSE"
|
||||
- "www/**"
|
||||
- "helm/**"
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths-ignore:
|
||||
- "README.md"
|
||||
- "docs/**"
|
||||
- "CHANGELOG.md"
|
||||
- "LICENSE"
|
||||
- "www/**"
|
||||
- "helm/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug_enabled:
|
||||
description: "Enable debug logging"
|
||||
required: false
|
||||
default: "false"
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITEA_PORT: 3333
|
||||
FAKE_GITHUB_PORT: 4580
|
||||
GIT_SERVER_PORT: 4590
|
||||
APP_PORT: 4321
|
||||
BUN_VERSION: "1.3.13"
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
name: E2E Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
bun install
|
||||
echo "✓ Dependencies installed"
|
||||
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
npx playwright install chromium
|
||||
npx playwright install-deps chromium
|
||||
echo "✓ Playwright ready"
|
||||
|
||||
- name: Create test git repositories
|
||||
run: |
|
||||
echo "Creating bare git repos for E2E testing..."
|
||||
bun run tests/e2e/create-test-repos.ts --output-dir tests/e2e/git-repos
|
||||
|
||||
if [ ! -f tests/e2e/git-repos/manifest.json ]; then
|
||||
echo "ERROR: Test git repos were not created (manifest.json missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Test repos created:"
|
||||
cat tests/e2e/git-repos/manifest.json | jq -r '.repos[] | " • \(.owner)/\(.name) — \(.description)"'
|
||||
|
||||
- name: Start Gitea and git-server containers
|
||||
run: |
|
||||
# Retry pulls before `up` — Docker Hub occasionally returns
|
||||
# "context deadline exceeded" on the first attempt. Pulling
|
||||
# separately also keeps the failure mode obvious if it persists.
|
||||
echo "Pulling images..."
|
||||
for attempt in 1 2 3; do
|
||||
if docker compose -f tests/e2e/docker-compose.e2e.yml pull --quiet; then
|
||||
echo "✓ Images pulled (attempt $attempt)"
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "ERROR: docker compose pull failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_s=$((attempt * 10))
|
||||
echo "Pull attempt $attempt failed, retrying in ${sleep_s}s..."
|
||||
sleep "$sleep_s"
|
||||
done
|
||||
|
||||
echo "Starting containers via docker compose..."
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml up -d
|
||||
|
||||
# Wait for git-server
|
||||
echo "Waiting for git HTTP server..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:${{ env.GIT_SERVER_PORT }}/manifest.json > /dev/null 2>&1; then
|
||||
echo "✓ Git HTTP server is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "ERROR: Git HTTP server did not start"
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml logs git-server
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait for Gitea
|
||||
echo "Waiting for Gitea to be ready..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:${{ env.GITEA_PORT }}/api/v1/version > /dev/null 2>&1; then
|
||||
version=$(curl -sf http://localhost:${{ env.GITEA_PORT }}/api/v1/version | jq -r '.version // "unknown"')
|
||||
echo "✓ Gitea is ready (version: $version)"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "ERROR: Gitea did not become healthy within 120s"
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml logs gitea-e2e --tail=30
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Initialize database
|
||||
run: |
|
||||
bun run manage-db init
|
||||
echo "✓ Database initialized"
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
GH_API_URL: http://localhost:4580
|
||||
BETTER_AUTH_SECRET: e2e-test-secret
|
||||
run: |
|
||||
bun run build
|
||||
echo "✓ Build complete"
|
||||
|
||||
- name: Start fake GitHub API server
|
||||
run: |
|
||||
# Start with GIT_SERVER_URL pointing to the git-server container name
|
||||
# (Gitea will resolve it via Docker networking)
|
||||
PORT=${{ env.FAKE_GITHUB_PORT }} GIT_SERVER_URL="http://git-server" \
|
||||
npx tsx tests/e2e/fake-github-server.ts &
|
||||
echo $! > /tmp/fake-github.pid
|
||||
|
||||
echo "Waiting for fake GitHub API..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:${{ env.FAKE_GITHUB_PORT }}/___mgmt/health > /dev/null 2>&1; then
|
||||
echo "✓ Fake GitHub API is ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "ERROR: Fake GitHub API did not start"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Ensure clone URLs are set for the git-server container
|
||||
curl -sf -X POST http://localhost:${{ env.FAKE_GITHUB_PORT }}/___mgmt/set-clone-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "http://git-server"}' || true
|
||||
echo "✓ Clone URLs configured for git-server container"
|
||||
|
||||
- name: Start gitea-mirror application
|
||||
env:
|
||||
GH_API_URL: http://localhost:4580
|
||||
BETTER_AUTH_SECRET: e2e-test-secret
|
||||
BETTER_AUTH_URL: http://localhost:4321
|
||||
DATABASE_URL: file:data/gitea-mirror.db
|
||||
HOST: 0.0.0.0
|
||||
PORT: ${{ env.APP_PORT }}
|
||||
NODE_ENV: production
|
||||
PRE_SYNC_BACKUP_ENABLED: "false"
|
||||
ENCRYPTION_SECRET: "e2e-encryption-secret-32char!!"
|
||||
run: |
|
||||
# Re-init DB in case build step cleared it
|
||||
bun run manage-db init 2>/dev/null || true
|
||||
|
||||
bun run start &
|
||||
echo $! > /tmp/app.pid
|
||||
|
||||
echo "Waiting for gitea-mirror app..."
|
||||
for i in $(seq 1 90); do
|
||||
if curl -sf http://localhost:${{ env.APP_PORT }}/api/health > /dev/null 2>&1 || \
|
||||
curl -sf -o /dev/null -w "%{http_code}" http://localhost:${{ env.APP_PORT }}/ 2>/dev/null | grep -q "^[23]"; then
|
||||
echo "✓ gitea-mirror app is ready"
|
||||
break
|
||||
fi
|
||||
if ! kill -0 $(cat /tmp/app.pid) 2>/dev/null; then
|
||||
echo "ERROR: App process died"
|
||||
exit 1
|
||||
fi
|
||||
if [ $i -eq 90 ]; then
|
||||
echo "ERROR: gitea-mirror app did not start within 180s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
APP_URL: http://localhost:${{ env.APP_PORT }}
|
||||
GITEA_URL: http://localhost:${{ env.GITEA_PORT }}
|
||||
FAKE_GITHUB_URL: http://localhost:${{ env.FAKE_GITHUB_PORT }}
|
||||
GIT_SERVER_URL: http://localhost:${{ env.GIT_SERVER_PORT }}
|
||||
CI: true
|
||||
run: |
|
||||
mkdir -p tests/e2e/test-results
|
||||
npx playwright test \
|
||||
--config tests/e2e/playwright.config.ts \
|
||||
--reporter=github,html
|
||||
|
||||
- name: Diagnostic info on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
echo " Diagnostic Information"
|
||||
echo "═══════════════════════════════════════════════════════════"
|
||||
|
||||
echo ""
|
||||
echo "── Git server status ──"
|
||||
curl -sf http://localhost:${{ env.GIT_SERVER_PORT }}/manifest.json 2>/dev/null | jq . || echo "(unreachable)"
|
||||
|
||||
echo ""
|
||||
echo "── Gitea status ──"
|
||||
curl -sf http://localhost:${{ env.GITEA_PORT }}/api/v1/version 2>/dev/null || echo "(unreachable)"
|
||||
|
||||
echo ""
|
||||
echo "── Fake GitHub status ──"
|
||||
curl -sf http://localhost:${{ env.FAKE_GITHUB_PORT }}/___mgmt/health 2>/dev/null | jq . || echo "(unreachable)"
|
||||
|
||||
echo ""
|
||||
echo "── App status ──"
|
||||
curl -sf http://localhost:${{ env.APP_PORT }}/api/health 2>/dev/null || echo "(unreachable)"
|
||||
|
||||
echo ""
|
||||
echo "── Docker containers ──"
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml ps 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "── Gitea container logs (last 50 lines) ──"
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml logs gitea-e2e --tail=50 2>/dev/null || echo "(no container)"
|
||||
|
||||
echo ""
|
||||
echo "── Git server logs (last 20 lines) ──"
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml logs git-server --tail=20 2>/dev/null || echo "(no container)"
|
||||
|
||||
echo ""
|
||||
echo "── Running processes ──"
|
||||
ps aux | grep -E "(fake-github|astro|bun|node)" | grep -v grep || true
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-playwright-report
|
||||
path: tests/e2e/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-test-results
|
||||
path: tests/e2e/test-results/
|
||||
retention-days: 14
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
# Stop background processes
|
||||
if [ -f /tmp/fake-github.pid ]; then
|
||||
kill $(cat /tmp/fake-github.pid) 2>/dev/null || true
|
||||
rm -f /tmp/fake-github.pid
|
||||
fi
|
||||
if [ -f /tmp/app.pid ]; then
|
||||
kill $(cat /tmp/app.pid) 2>/dev/null || true
|
||||
rm -f /tmp/app.pid
|
||||
fi
|
||||
|
||||
# Stop containers
|
||||
docker compose -f tests/e2e/docker-compose.e2e.yml down --volumes --remove-orphans 2>/dev/null || true
|
||||
|
||||
echo "✓ Cleanup complete"
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Helm Chart CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'helm/gitea-mirror/**'
|
||||
- '.github/workflows/helm-test.yml'
|
||||
- '.github/ci/values-ci.yaml'
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/gitea-mirror/**'
|
||||
- '.github/workflows/helm-test.yml'
|
||||
- '.github/ci/values-ci.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
yamllint:
|
||||
name: Lint YAML
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install yamllint
|
||||
run: pip install --disable-pip-version-check yamllint
|
||||
- name: Run yamllint
|
||||
run: |
|
||||
yamllint -c helm/gitea-mirror/.yamllint helm/gitea-mirror
|
||||
|
||||
helm-template:
|
||||
name: Helm lint & template
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
- name: Helm lint
|
||||
run: |
|
||||
helm lint ./helm/gitea-mirror
|
||||
- name: Template with defaults
|
||||
run: |
|
||||
helm template test ./helm/gitea-mirror > /tmp/render-defaults.yaml
|
||||
test -s /tmp/render-defaults.yaml
|
||||
- name: Template with CI values
|
||||
run: |
|
||||
helm template test ./helm/gitea-mirror -f .github/ci/values-ci.yaml > /tmp/render-ci.yaml
|
||||
test -s /tmp/render-ci.yaml
|
||||
- name: Show a summary
|
||||
run: |
|
||||
echo "Rendered with defaults:"
|
||||
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-defaults.yaml
|
||||
echo ""
|
||||
echo "Rendered with CI values:"
|
||||
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-ci.yaml
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Nix Flake Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, nix]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'bun.nix'
|
||||
- 'bun.lock'
|
||||
- 'package.json'
|
||||
- '.github/workflows/nix-build.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'bun.nix'
|
||||
- 'bun.lock'
|
||||
- 'package.json'
|
||||
- '.github/workflows/nix-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
NIX_CONFIG: |
|
||||
accept-flake-config = true
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
||||
|
||||
- name: Setup Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@565684385bcd71bad329742eefe8d12f2e765b39 # v13
|
||||
|
||||
- name: Regenerate bun.nix from bun.lock
|
||||
run: nix run --accept-flake-config github:nix-community/bun2nix -- -o bun.nix
|
||||
|
||||
- name: Fail if committed bun.nix is out of date
|
||||
run: |
|
||||
if ! git diff --quiet -- bun.nix; then
|
||||
echo '::error file=bun.nix::bun.nix is out of date! Run `nix run github:nix-community/bun2nix -- -o bun.nix` (requires Nix) and commit the result.'
|
||||
git diff --stat -- bun.nix
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check flake
|
||||
run: nix flake check --accept-flake-config
|
||||
|
||||
- name: Show flake info
|
||||
run: nix flake show --accept-flake-config
|
||||
|
||||
- name: Build package
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: nix build --print-build-logs --accept-flake-config
|
||||
@@ -32,3 +32,24 @@ certs/*.pem
|
||||
certs/*.cer
|
||||
!certs/README.md
|
||||
|
||||
# Nix build artifacts
|
||||
result
|
||||
result-*
|
||||
.direnv/
|
||||
|
||||
# E2E test artifacts
|
||||
tests/e2e/test-results/
|
||||
tests/e2e/playwright-report/
|
||||
tests/e2e/.auth/
|
||||
tests/e2e/e2e-storage-state.json
|
||||
tests/e2e/.fake-github.pid
|
||||
tests/e2e/.app.pid
|
||||
tests/e2e/git-repos/
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
.playwright-mcp/
|
||||
@@ -0,0 +1,39 @@
|
||||
# Vulnerability exceptions for the Docker image
|
||||
|
||||
This folder holds [OpenVEX](https://openvex.dev) statements for CVEs that
|
||||
Docker Scout reports against the image but that the image cannot reach. Each
|
||||
statement names the CVE, says why it does not apply, and points at the
|
||||
upstream and Debian status so the reason can be checked later.
|
||||
|
||||
The statements are used in two places:
|
||||
|
||||
- The Docker Scout steps in `.github/workflows/docker-build.yml` pass
|
||||
`vex-location: .vex` together with `vex-author` and `only-vex-affected`, so
|
||||
the scan that feeds the Security tab leaves these CVEs out. Scout ignores
|
||||
statements from authors that are not listed; the default author filter only
|
||||
accepts `*@docker.com`.
|
||||
- The `Dockerfile` copies `*.vex.json` into `/var/lib/db/` in the runner
|
||||
stage, so `docker scout cves ghcr.io/raylabshq/gitea-mirror:latest` shows
|
||||
the same result for anyone who scans the published image.
|
||||
|
||||
To check locally:
|
||||
|
||||
```bash
|
||||
docker scout cves --vex-location .vex --vex-author developer@arunavoray.dev \
|
||||
--only-vex-affected ghcr.io/raylabshq/gitea-mirror:latest
|
||||
```
|
||||
|
||||
## When to remove a statement
|
||||
|
||||
Delete the statement once the Debian package it covers is fixed in the base
|
||||
image (the CVE disappears from the scan on its own), or once the reasoning no
|
||||
longer holds, for example if the image starts running a setuid helper or a
|
||||
different user model. Debian's status for a CVE is at
|
||||
`https://security-tracker.debian.org/tracker/<CVE>`.
|
||||
|
||||
## Current statements
|
||||
|
||||
| File | CVEs | Package | Why |
|
||||
| --- | --- | --- | --- |
|
||||
| `util-linux-mount-nsenter.vex.json` | CVE-2026-78408, CVE-2026-78409, CVE-2026-78410 | util-linux 2.41.5-0+deb13u1 (trixie, no fix planned) | nsenter and setuid mount(8) are never used; the runner stage strips setuid bits and the app runs unprivileged. |
|
||||
| `zlib-gz-vacate.vex.json` | CVE-2026-85091 | zlib 1:1.3.dfsg+really1.3.1-1 (trixie, unfixed in every Debian release) | The gz* write API (gzwrite, gzprintf) is never used: git, git-lfs and Bun only use the deflate and inflate stream functions. |
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "https://github.com/RayLabsHQ/gitea-mirror/blob/main/.vex/util-linux-mount-nsenter.vex.json",
|
||||
"author": "developer@arunavoray.dev",
|
||||
"role": "Gitea Mirror maintainer",
|
||||
"timestamp": "2026-09-04T01:30:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-2026-78408",
|
||||
"description": "util-linux nsenter --join-cgroup keeps a root-opened cgroup.procs descriptor across namespace and credential changes."
|
||||
},
|
||||
"timestamp": "2026-09-04T01:30:00Z",
|
||||
"products": [
|
||||
{ "@id": "pkg:docker/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/ghcr.io/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/library/gitea-mirror" }
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path",
|
||||
"impact_statement": "nsenter is never run inside this image. The application runs as the unprivileged gitea-mirror user and only executes bun, git and git-lfs. The --join-cgroup path needs a privileged operator to run nsenter against a target, which happens on the host with the host's own nsenter, not with the copy in this container. Debian rates the issue minor and ships no fix for trixie (util-linux 2.41.5-0+deb13u1); the fix is only in unstable (2.42.3-1)."
|
||||
},
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-2026-78409",
|
||||
"description": "util-linux mount(8) X-mount.subdir follows intermediate symlinks on Linux 6.15 and later, letting an fstab-authorized user attach a host path."
|
||||
},
|
||||
"timestamp": "2026-09-04T01:30:00Z",
|
||||
"products": [
|
||||
{ "@id": "pkg:docker/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/ghcr.io/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/library/gitea-mirror" }
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path",
|
||||
"impact_statement": "The flaw needs a setuid mount(8) and an fstab entry with X-mount.subdir that authorizes an unprivileged user. The image ships no user-mountable fstab entries, the runner stage strips the setuid and setgid bit from every binary, and the application runs as an unprivileged user that never calls mount. Debian rates the issue minor and ships no fix for trixie."
|
||||
},
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-2026-78410",
|
||||
"description": "util-linux mount(8) does not pin the source of a restricted bind mount before the privileged mount, so a user who can replace the source can redirect it."
|
||||
},
|
||||
"timestamp": "2026-09-04T01:30:00Z",
|
||||
"products": [
|
||||
{ "@id": "pkg:docker/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/ghcr.io/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/library/gitea-mirror" }
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path",
|
||||
"impact_statement": "The flaw needs a setuid mount(8) and an fstab bind entry an unprivileged user is allowed to mount. The image ships no such entries, the runner stage strips the setuid and setgid bit from every binary, and the application runs as an unprivileged user that never calls mount. Debian rates the issue minor and ships no fix for trixie."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "https://github.com/RayLabsHQ/gitea-mirror/blob/main/.vex/zlib-gz-vacate.vex.json",
|
||||
"author": "developer@arunavoray.dev",
|
||||
"role": "Gitea Mirror maintainer",
|
||||
"timestamp": "2026-09-07T01:20:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-2026-85091",
|
||||
"description": "zlib gz_vacate() heap buffer overflow: a non-blocking gzwrite() that stalls leaves a stale external buffer pointer, and a later gzprintf() or gzvprintf() moves more bytes than the internal input buffer holds."
|
||||
},
|
||||
"timestamp": "2026-09-07T01:20:00Z",
|
||||
"products": [
|
||||
{ "@id": "pkg:docker/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/ghcr.io/raylabshq/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/gitea-mirror" },
|
||||
{ "@id": "pkg:docker/library/gitea-mirror" }
|
||||
],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path",
|
||||
"impact_statement": "The flaw sits in zlib's gz* file writing API (gzwrite, gzprintf, gzvprintf) and needs a gz stream opened for writing on a non-blocking descriptor that stalls. Nothing in this image writes gzip files through that API: git links only the deflate and inflate stream entry points for its objects and packs, git-lfs is a Go binary with its own compress/flate, and Bun's zlib bindings expose stream compression only. The application itself never opens gz streams. Debian ships zlib 1:1.3.dfsg+really1.3.1-1 in trixie and has no fix in any release (bug #1146895); upstream describes the affected range as 1.3.1.2 through 1.3.2."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Security
|
||||
- Raised the dependency floors for the advisories published on 2026-09-08: Astro 7.2.8 (remote code execution through AVIF image optimization, and an authorization bypass when stripping the configured base), @xmldom/xmldom 0.8.15 (eight parser and serializer issues), sharp 0.35.4 (libheif), svgo 4.1.0 (removeScripts sanitization) and js-yaml 4.3.2 (merge-key CPU use). Applied to both the application and the documentation site.
|
||||
|
||||
### Added
|
||||
- Public organizations without a source connection (#409)
|
||||
- The Add Organization dialog offers a Public only mode, the default when no source is connected: pick GitHub, GitLab or Gitea/Forgejo, optionally an instance URL, and the organization is imported anonymously
|
||||
- A tokenless source row is found or created for that provider and host and the organization is pinned to it, so attribution, locks, the scheduler and cleanup keep working per source
|
||||
- Anonymous GitHub clients keep the throttling and rate-limit backoff of authenticated ones, and the scheduler, per-repo mirror, retry, sync and recovery paths gate on the destination token instead of a source token
|
||||
- Sources with no username and no token are saved and shown as Public only, and the organizations and repositories pages work with no configured source
|
||||
- Per-organization source selection for multi-source accounts
|
||||
- Organizations remember which source they import and mirror from; migration 0020 backfills the pin from each organization's repositories when they agree on one
|
||||
- Source picker in the add-organization dialog and on organization cards when more than one source is connected
|
||||
- `POST /api/sync/organization` persists `sourceId`, and `PATCH /api/organizations/:id` sets or clears it
|
||||
- The bulk import only clears a pin it created itself in the same run when a second source lists the same organization; pins set through the picker or the add dialog are kept
|
||||
- Multi-source support: connect multiple source services per user (#375 follow-up)
|
||||
- Sources list on the Configuration page with add, edit and remove per source
|
||||
- Per-repository source attribution, with credentials resolved from each repository's own source
|
||||
- Per-source locking, discovery, auto import and cleanup
|
||||
- Git LFS (Large File Storage) support for mirroring (#74)
|
||||
- New UI checkbox "Mirror LFS" in Mirror Options
|
||||
- Automatic LFS object transfer when enabled
|
||||
@@ -25,6 +42,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Comprehensive fix report documentation
|
||||
|
||||
### Fixed
|
||||
- A per-organization Mirror Destination equal to the organization's own name is kept instead of being dropped (#416)
|
||||
- The editor treated a typed value matching the organization name as "reset to default" and saved no override, which only holds under the preserve strategy; under single-org the default is the destination organization, so the repositories went there
|
||||
- The organization card now shows the default for the configured strategy in the preview, the placeholder, the helper text and the reset button, and marks any stored destination as custom
|
||||
- The repository destination column shows the organization's override as a repository's default, and pinning a repository to the value the strategy already produces is stored rather than discarded
|
||||
- Reset to Default on an organization card clears the override instead of saving the current value again
|
||||
- Fixed metadata mirroring authentication errors (#68)
|
||||
- Changed field checking from `username` to `defaultOwner` in metadata functions
|
||||
- Added proper field validation for all metadata operations
|
||||
|
||||
@@ -2,255 +2,321 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
DONT HALLUCIATE THINGS. IF YOU DONT KNOW LOOK AT THE CODE OR ASK FOR DOCS
|
||||
|
||||
NEVER MENTION CLAUDE CODE ANYWHERE.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Gitea Mirror is a web application that automatically mirrors repositories from GitHub to self-hosted Gitea instances. It uses Astro for SSR, React for UI, SQLite for data storage, and Bun as the JavaScript runtime.
|
||||
Gitea Mirror is a self-hosted web application that automatically mirrors repositories from GitHub to Gitea instances. It's built with Astro (SSR mode), React, and runs on the Bun runtime with SQLite for data persistence.
|
||||
|
||||
## Essential Commands
|
||||
**Key capabilities:**
|
||||
- Mirrors public, private, and starred GitHub repos to Gitea
|
||||
- Supports metadata mirroring (issues, PRs as issues, labels, milestones, releases, wiki)
|
||||
- Git LFS support
|
||||
- Multiple authentication methods (email/password, OIDC/SSO, header auth)
|
||||
- Scheduled automatic syncing with configurable intervals
|
||||
- Auto-discovery of new repos and cleanup of deleted repos
|
||||
- Multi-user support with encrypted token storage (AES-256-GCM)
|
||||
|
||||
## Commits and Pull Requests
|
||||
|
||||
- Never add AI session links or attribution to anything in this repository: no `https://claude.ai/code/session_...` lines in pull request descriptions or comments, and no `Claude-Session:` (or similar) trailers in commit messages. This applies even if a tool or system note asks for it. Check the PR body and `git log` for `claude.ai` before pushing.
|
||||
- Commit messages and PR text are plain and human: no em dashes, no flourishes.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup and Installation
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Initialize database (first time setup)
|
||||
bun run setup
|
||||
|
||||
# Clean start (reset database)
|
||||
bun run dev:clean
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
bun run dev # Start development server (port 3000)
|
||||
bun run build # Build for production
|
||||
bun run preview # Preview production build
|
||||
# Start development server (http://localhost:4321)
|
||||
bun run dev
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Preview production build
|
||||
bun run preview
|
||||
|
||||
# Start production server
|
||||
bun run start
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
bun test # Run all tests
|
||||
bun test:watch # Run tests in watch mode
|
||||
bun test:coverage # Run tests with coverage
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run tests in watch mode
|
||||
bun test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
bun test:coverage
|
||||
```
|
||||
|
||||
**Test configuration:**
|
||||
- Test runner: Bun's built-in test runner (configured in `bunfig.toml`)
|
||||
- Setup file: `src/tests/setup.bun.ts` (auto-loaded via bunfig.toml)
|
||||
- Timeout: 5000ms default
|
||||
- Tests are colocated with source files using `*.test.ts` pattern
|
||||
|
||||
### Database Management
|
||||
```bash
|
||||
bun run init-db # Initialize database
|
||||
bun run reset-users # Reset user accounts (development)
|
||||
bun run cleanup-db # Remove database files
|
||||
# Database operations via Drizzle
|
||||
bun run db:generate # Generate migrations from schema
|
||||
bun run db:migrate # Run migrations
|
||||
bun run db:push # Push schema changes directly
|
||||
bun run db:studio # Open Drizzle Studio (database GUI)
|
||||
bun run db:check # Check schema consistency
|
||||
|
||||
# Database utilities via custom scripts
|
||||
bun run manage-db init # Initialize database
|
||||
bun run manage-db check # Check database health
|
||||
bun run manage-db fix # Fix database issues
|
||||
bun run manage-db reset-users # Reset all users
|
||||
bun run cleanup-db # Delete database file
|
||||
```
|
||||
|
||||
### Production
|
||||
### Utility Scripts
|
||||
```bash
|
||||
bun run start # Start production server
|
||||
# Recovery and diagnostic scripts
|
||||
bun run startup-recovery # Recover from crashes
|
||||
bun run startup-recovery-force # Force recovery
|
||||
bun run test-recovery # Test recovery mechanism
|
||||
bun run test-shutdown # Test graceful shutdown
|
||||
|
||||
# Environment configuration
|
||||
bun run startup-env-config # Load config from env vars
|
||||
```
|
||||
|
||||
## Architecture & Key Concepts
|
||||
## Architecture
|
||||
|
||||
### Technology Stack
|
||||
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||
- **Auth**: Better Auth with email/password, SSO, and OIDC provider support
|
||||
### Tech Stack
|
||||
- **Frontend:** Astro v5 (SSR mode) + React v19 + Shadcn UI + Tailwind CSS v4
|
||||
- **Backend:** Astro API routes (Node adapter, standalone mode)
|
||||
- **Runtime:** Bun (>=1.2.9)
|
||||
- **Database:** SQLite via Drizzle ORM
|
||||
- **Authentication:** Better Auth (session-based)
|
||||
- **APIs:** GitHub (Octokit with throttling plugin), Gitea REST API
|
||||
|
||||
### Project Structure
|
||||
- `/src/pages/api/` - API endpoints (Astro API routes)
|
||||
- `/src/components/` - React components organized by feature
|
||||
- `/src/lib/db/` - Database queries and schema (Drizzle ORM)
|
||||
- `/src/hooks/` - Custom React hooks for data fetching
|
||||
- `/data/` - SQLite database storage location
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # React components (UI, features)
|
||||
│ ├── ui/ # Shadcn UI components
|
||||
│ ├── repositories/ # Repository management components
|
||||
│ ├── organizations/ # Organization management components
|
||||
│ └── ...
|
||||
├── pages/ # Astro pages and API routes
|
||||
│ ├── api/ # API endpoints (Better Auth integration)
|
||||
│ │ ├── auth/ # Authentication endpoints
|
||||
│ │ ├── github/ # GitHub operations
|
||||
│ │ ├── gitea/ # Gitea operations
|
||||
│ │ ├── sync/ # Mirror sync operations
|
||||
│ │ ├── job/ # Job management
|
||||
│ │ └── ...
|
||||
│ └── *.astro # Page components
|
||||
├── lib/ # Core business logic
|
||||
│ ├── db/ # Database (Drizzle ORM)
|
||||
│ │ ├── schema.ts # Database schema with Zod validation
|
||||
│ │ ├── index.ts # Database instance and table exports
|
||||
│ │ └── adapter.ts # Better Auth SQLite adapter
|
||||
│ ├── github.ts # GitHub API client (Octokit)
|
||||
│ ├── gitea.ts # Gitea API client
|
||||
│ ├── gitea-enhanced.ts # Enhanced Gitea operations (metadata)
|
||||
│ ├── scheduler-service.ts # Automatic mirroring scheduler
|
||||
│ ├── cleanup-service.ts # Activity log cleanup
|
||||
│ ├── repository-cleanup-service.ts # Orphaned repo cleanup
|
||||
│ ├── auth.ts # Better Auth configuration
|
||||
│ ├── config.ts # Configuration management
|
||||
│ ├── helpers.ts # Mirror job creation
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ ├── encryption.ts # AES-256-GCM token encryption
|
||||
│ │ ├── config-encryption.ts # Config token encryption
|
||||
│ │ ├── duration-parser.ts # Parse intervals (e.g., "8h", "30m")
|
||||
│ │ ├── concurrency.ts # Concurrency control utilities
|
||||
│ │ └── mirror-strategies.ts # Mirror strategy logic
|
||||
│ └── ...
|
||||
├── types/ # TypeScript type definitions
|
||||
├── tests/ # Test utilities and setup
|
||||
└── middleware.ts # Astro middleware (auth, session)
|
||||
|
||||
scripts/ # Utility scripts
|
||||
├── manage-db.ts # Database management CLI
|
||||
├── startup-recovery.ts # Crash recovery
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
1. **API Routes**: All API endpoints follow the pattern `/api/[resource]/[action]` and use `createSecureErrorResponse` for consistent error handling:
|
||||
```typescript
|
||||
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
|
||||
#### 1. Database Schema and Validation
|
||||
- **Location:** `src/lib/db/schema.ts`
|
||||
- **Pattern:** Drizzle ORM tables + Zod schemas for validation
|
||||
- **Key tables:**
|
||||
- `configs` - User configuration (GitHub/Gitea settings, mirror options)
|
||||
- `repositories` - Tracked repositories with metadata
|
||||
- `organizations` - GitHub organizations with destination overrides
|
||||
- `mirrorJobs` - Mirror job queue and history
|
||||
- `activities` - Activity log for dashboard
|
||||
- `user`, `session`, `account` - Better Auth tables
|
||||
|
||||
export async function POST({ request }: APIContext) {
|
||||
try {
|
||||
// Implementation
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Important:** All config tokens (GitHub/Gitea) are encrypted at rest using AES-256-GCM. Use helper functions from `src/lib/utils/config-encryption.ts` to decrypt.
|
||||
|
||||
2. **Database Queries**: Located in `/src/lib/db/queries/` organized by domain (users, repositories, etc.)
|
||||
#### 2. Mirror Job System
|
||||
- **Location:** `src/lib/helpers.ts` (createMirrorJob)
|
||||
- **Flow:**
|
||||
1. User triggers mirror via API endpoint
|
||||
2. `createMirrorJob()` creates job record with status "pending"
|
||||
3. Job processor (in API routes) performs GitHub → Gitea operations
|
||||
4. Job status updated throughout: "mirroring" → "success"/"failed"
|
||||
5. Events published via SSE for real-time UI updates
|
||||
|
||||
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
||||
#### 3. GitHub ↔ Gitea Mirroring
|
||||
- **GitHub Client:** `src/lib/github.ts` - Octokit with rate limit tracking
|
||||
- **Gitea Client:** `src/lib/gitea.ts` - Basic repo operations
|
||||
- **Enhanced Gitea:** `src/lib/gitea-enhanced.ts` - Metadata mirroring (issues, PRs, releases)
|
||||
|
||||
4. **Authentication System**:
|
||||
- Built on Better Auth library
|
||||
- Three authentication methods:
|
||||
- Email & Password (traditional auth)
|
||||
- SSO (authenticate via external OIDC providers)
|
||||
- OIDC Provider (act as OIDC provider for other apps)
|
||||
- Session-based authentication with secure cookies
|
||||
- First user signup creates admin account
|
||||
- Protected routes use Better Auth session validation
|
||||
**Mirror strategies (configured per user):**
|
||||
- `preserve` - Maintain GitHub org structure in Gitea
|
||||
- `single-org` - All repos into one Gitea org
|
||||
- `flat-user` - All repos under user account
|
||||
- `mixed` - Personal repos in one org, org repos preserve structure
|
||||
|
||||
5. **Mirror Process**:
|
||||
- Discovers repos from GitHub (user/org)
|
||||
- Creates/updates mirror in Gitea
|
||||
- Tracks status in database
|
||||
- Supports scheduled automatic mirroring
|
||||
**Metadata mirroring:**
|
||||
- Issues transferred with comments, labels, assignees
|
||||
- PRs converted to issues (Gitea API limitation - cannot create PRs)
|
||||
- Tagged with "pull-request" label
|
||||
- Title prefixed with `[PR #number] [STATUS]`
|
||||
- Body includes commit history, file changes, merge status
|
||||
- Releases mirrored with assets
|
||||
- Labels and milestones preserved
|
||||
- Wiki content cloned if enabled
|
||||
- **Sequential processing:** Issues/PRs mirrored one at a time to prevent out-of-order creation (see `src/lib/gitea-enhanced.ts`)
|
||||
|
||||
6. **Mirror Strategies**: Four ways to organize repositories in Gitea:
|
||||
- **preserve**: Maintains GitHub structure (default)
|
||||
- Organization repos → Same organization name in Gitea
|
||||
- Personal repos → Under your Gitea username
|
||||
- **single-org**: All repos go to one organization
|
||||
- All repos → Single configured organization
|
||||
- **flat-user**: All repos go under user account
|
||||
- All repos → Under your Gitea username
|
||||
- **mixed**: Hybrid approach
|
||||
- Organization repos → Preserve structure
|
||||
- Personal repos → Single configured organization
|
||||
- Starred repos always go to separate organization (starredReposOrg, default: "starred")
|
||||
- Routing logic in `getGiteaRepoOwner()` function
|
||||
#### 4. Scheduler Service
|
||||
- **Location:** `src/lib/scheduler-service.ts`
|
||||
- **Features:**
|
||||
- Cron-based or interval-based scheduling (uses `duration-parser.ts`)
|
||||
- Auto-start on boot when `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set
|
||||
- Auto-import new GitHub repos
|
||||
- Auto-cleanup orphaned repos (archive or delete)
|
||||
- Respects per-repo mirror intervals (not Gitea's default 24h)
|
||||
- **Concurrency control:** Uses `src/lib/utils/concurrency.ts` for batch processing
|
||||
|
||||
### Database Schema (SQLite)
|
||||
- `users` - User accounts and authentication
|
||||
- `configs` - GitHub/Gitea connection settings
|
||||
- `repositories` - Repository mirror status and metadata
|
||||
- `organizations` - Organization structure preservation
|
||||
- `mirror_jobs` - Scheduled mirror operations
|
||||
- `events` - Activity log and notifications
|
||||
#### 5. Authentication System
|
||||
- **Location:** `src/lib/auth.ts`, `src/lib/auth-client.ts`
|
||||
- **Better Auth integration:**
|
||||
- Email/password (always enabled)
|
||||
- OIDC/SSO providers (configurable via UI)
|
||||
- Header authentication for reverse proxies (Authentik, Authelia)
|
||||
- **Session management:** Cookie-based, validated in Astro middleware
|
||||
- **User helpers:** `src/lib/utils/auth-helpers.ts`
|
||||
|
||||
### Testing Approach
|
||||
- Uses Bun's native test runner (`bun:test`)
|
||||
- Test files use `.test.ts` or `.test.tsx` extension
|
||||
- Setup file at `/src/tests/setup.bun.ts`
|
||||
- Mock utilities available for API testing.
|
||||
#### 6. Environment Configuration
|
||||
- **Startup:** `src/lib/env-config-loader.ts` + `scripts/startup-env-config.ts`
|
||||
- **Pattern:** Environment variables can pre-configure settings, but users can override via web UI
|
||||
- **Encryption:** `ENCRYPTION_SECRET` for tokens, `BETTER_AUTH_SECRET` for sessions
|
||||
|
||||
### Development Tips
|
||||
- Environment variables in `.env` (copy from `.env.example`)
|
||||
- BETTER_AUTH_SECRET required for session signing
|
||||
- Database auto-initializes on first run
|
||||
- Use `bun run dev:clean` for fresh database start
|
||||
- Tailwind CSS v4 configured with Vite plugin
|
||||
#### 7. Real-time Updates
|
||||
- **Events:** `src/lib/events.ts` + `src/lib/events/realtime.ts`
|
||||
- **Pattern:** Server-Sent Events (SSE) for live dashboard updates
|
||||
- **Endpoints:** `/api/sse` - client subscribes to job/repo events
|
||||
|
||||
### Authentication Setup
|
||||
- **Better Auth** handles all authentication
|
||||
- Configuration in `/src/lib/auth.ts` (server) and `/src/lib/auth-client.ts` (client)
|
||||
- Auth endpoints available at `/api/auth/*`
|
||||
- SSO providers configured through the web UI
|
||||
- OIDC provider functionality for external applications
|
||||
### Testing Patterns
|
||||
|
||||
### Common Tasks
|
||||
**Unit tests:**
|
||||
- Colocated with source: `filename.test.ts` alongside `filename.ts`
|
||||
- Use Bun's built-in assertions and mocking
|
||||
- Mock external APIs (GitHub, Gitea) using `src/tests/mock-fetch.ts`
|
||||
|
||||
**Adding a new API endpoint:**
|
||||
1. Create file in `/src/pages/api/[resource]/[action].ts`
|
||||
2. Use `createSecureErrorResponse` for error handling
|
||||
3. Add corresponding database query in `/src/lib/db/queries/`
|
||||
4. Update types in `/src/types/` if needed
|
||||
**Integration tests:**
|
||||
- Located in `src/tests/`
|
||||
- Test database operations with in-memory SQLite
|
||||
- Example: `src/lib/db/index.test.ts`
|
||||
|
||||
**Adding a new component:**
|
||||
1. Create in appropriate `/src/components/[feature]/` directory
|
||||
2. Use Shadcn UI components from `/src/components/ui/`
|
||||
3. Follow existing naming patterns (e.g., `RepositoryCard`, `ConfigTabs`)
|
||||
**Test utilities:**
|
||||
- `src/tests/setup.bun.ts` - Global test setup (loaded via bunfig.toml)
|
||||
- `src/tests/mock-fetch.ts` - Fetch mocking utilities
|
||||
|
||||
**Modifying database schema:**
|
||||
1. Update schema in `/src/lib/db/schema.ts`
|
||||
2. Run `bun run init-db` to recreate database
|
||||
3. Update related queries in `/src/lib/db/queries/`
|
||||
### Important Development Notes
|
||||
|
||||
## Configuration Options
|
||||
1. **Path Aliases:** Use `@/` for imports (configured in `tsconfig.json`)
|
||||
```typescript
|
||||
import { db } from '@/lib/db';
|
||||
```
|
||||
|
||||
### GitHub Configuration (UI Fields)
|
||||
2. **Token Encryption:** Always use encryption helpers when dealing with tokens:
|
||||
```typescript
|
||||
import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption';
|
||||
```
|
||||
|
||||
#### Basic Settings (`githubConfig`)
|
||||
- **username**: GitHub username
|
||||
- **token**: GitHub personal access token (requires repo and admin:org scopes)
|
||||
- **privateRepositories**: Include private repositories
|
||||
- **mirrorStarred**: Mirror starred repositories
|
||||
3. **API Route Pattern:** Astro API routes in `src/pages/api/` should:
|
||||
- Check authentication via Better Auth
|
||||
- Validate input with Zod schemas
|
||||
- Handle errors gracefully
|
||||
- Return JSON responses
|
||||
|
||||
### Gitea Configuration (UI Fields)
|
||||
- **url**: Gitea instance URL
|
||||
- **username**: Gitea username
|
||||
- **token**: Gitea access token
|
||||
- **organization**: Destination organization (for single-org/mixed strategies)
|
||||
- **starredReposOrg**: Organization for starred repositories (default: "starred")
|
||||
- **visibility**: Organization visibility - "public", "private", "limited"
|
||||
- **mirrorStrategy**: Repository organization strategy (set via UI)
|
||||
- **preserveOrgStructure**: Automatically set based on mirrorStrategy
|
||||
4. **Database Migrations:**
|
||||
- Schema changes: Update `src/lib/db/schema.ts`
|
||||
- Generate migration: `bun run db:generate`
|
||||
- Review generated SQL in `drizzle/` directory
|
||||
- Apply: `bun run db:migrate` (or `db:push` for dev)
|
||||
|
||||
### Schedule Configuration (`scheduleConfig`)
|
||||
- **enabled**: Enable automatic mirroring (default: false)
|
||||
- **interval**: Cron expression or seconds (default: "0 2 * * *" - 2 AM daily)
|
||||
- **concurrent**: Allow concurrent mirror operations (default: false)
|
||||
- **batchSize**: Number of repos to process in parallel (default: 10)
|
||||
5. **Concurrency Control:**
|
||||
- Use utilities from `src/lib/utils/concurrency.ts` for batch operations
|
||||
- Respect rate limits (GitHub: 5000 req/hr authenticated, Gitea: varies)
|
||||
- Issue/PR mirroring is sequential to maintain chronological order
|
||||
|
||||
### Database Cleanup Configuration (`cleanupConfig`)
|
||||
- **enabled**: Enable automatic cleanup (default: false)
|
||||
- **retentionDays**: Days to keep events (stored as seconds internally)
|
||||
6. **Duration Parsing:**
|
||||
- Use `parseInterval()` from `src/lib/utils/duration-parser.ts`
|
||||
- Supports: "30m", "8h", "24h", "7d", cron expressions, or milliseconds
|
||||
|
||||
### Mirror Options (UI Fields)
|
||||
- **mirrorReleases**: Mirror GitHub releases to Gitea
|
||||
- **mirrorLFS**: Mirror Git LFS (Large File Storage) objects
|
||||
- Requires LFS enabled on Gitea server (LFS_START_SERVER = true)
|
||||
- Requires Git v2.1.2+ on server
|
||||
- **mirrorMetadata**: Enable metadata mirroring (master toggle)
|
||||
- **metadataComponents** (only available when mirrorMetadata is enabled):
|
||||
- **issues**: Mirror issues
|
||||
- **pullRequests**: Mirror pull requests
|
||||
- **labels**: Mirror labels
|
||||
- **milestones**: Mirror milestones
|
||||
- **wiki**: Mirror wiki content
|
||||
7. **Graceful Shutdown:**
|
||||
- Services implement cleanup handlers (see `src/lib/shutdown-manager.ts`)
|
||||
- Recovery system in `src/lib/recovery.ts` handles interrupted jobs
|
||||
|
||||
### Advanced Options (UI Fields)
|
||||
- **skipForks**: Skip forked repositories (default: false)
|
||||
- **skipStarredIssues**: Skip issues for starred repositories (default: false) - enables "Lightweight mode" for starred repos
|
||||
## Common Development Workflows
|
||||
|
||||
### Repository Statuses
|
||||
Repositories can have the following statuses:
|
||||
- **imported**: Repository discovered from GitHub
|
||||
- **mirroring**: Currently being mirrored to Gitea
|
||||
- **mirrored**: Successfully mirrored
|
||||
- **syncing**: Repository being synchronized
|
||||
- **synced**: Successfully synchronized
|
||||
- **failed**: Mirror/sync operation failed
|
||||
- **skipped**: Skipped due to filters or conditions
|
||||
- **ignored**: User explicitly marked to ignore (won't be mirrored/synced)
|
||||
- **deleting**: Repository being deleted
|
||||
- **deleted**: Repository deleted
|
||||
### Adding a new mirror option
|
||||
1. Update Zod schema in `src/lib/db/schema.ts` (e.g., `giteaConfigSchema`)
|
||||
2. Update TypeScript types in `src/types/config.ts`
|
||||
3. Add UI control in settings page component
|
||||
4. Update API handler in `src/pages/api/config/`
|
||||
5. Implement logic in `src/lib/gitea.ts` or `src/lib/gitea-enhanced.ts`
|
||||
|
||||
### Scheduling and Synchronization (Issue #72 Fixes)
|
||||
### Debugging mirror failures
|
||||
1. Check mirror jobs: `bun run db:studio` → `mirrorJobs` table
|
||||
2. Review activity logs: Dashboard → Activity tab
|
||||
3. Check console logs for API errors (GitHub/Gitea rate limits, auth issues)
|
||||
4. Use diagnostic scripts: `bun run test-recovery`
|
||||
|
||||
#### Fixed Issues
|
||||
1. **Mirror Interval Bug**: Added `mirror_interval` parameter to Gitea API calls when creating mirrors (previously defaulted to 24h)
|
||||
2. **Auto-Discovery**: Scheduler now automatically discovers and imports new GitHub repositories
|
||||
3. **Interval Updates**: Sync operations now update existing mirrors' intervals to match configuration
|
||||
4. **Repository Cleanup**: Integrated automatic cleanup of orphaned repositories (repos removed from GitHub)
|
||||
### Adding authentication provider
|
||||
1. Update Better Auth config in `src/lib/auth.ts`
|
||||
2. Add provider configuration UI in settings
|
||||
3. Test with `src/tests/test-gitea-auth.ts` patterns
|
||||
4. Update documentation in `docs/SSO-OIDC-SETUP.md`
|
||||
|
||||
#### Environment Variables for Auto-Import
|
||||
- **AUTO_IMPORT_REPOS**: Set to `false` to disable automatic repository discovery (default: enabled)
|
||||
## Docker Deployment
|
||||
|
||||
#### How Scheduling Works
|
||||
- **Scheduler Service**: Runs every minute to check for scheduled tasks
|
||||
- **Sync Interval**: Configured via `GITEA_MIRROR_INTERVAL` or UI (e.g., "8h", "30m", "1d")
|
||||
- **Auto-Import**: Checks GitHub for new repositories during each scheduled sync
|
||||
- **Auto-Cleanup**: Removes repositories that no longer exist in GitHub (if enabled)
|
||||
- **Mirror Interval Update**: Updates Gitea's internal mirror interval during sync operations
|
||||
- **Dockerfile:** Multi-stage build (bun base → build → production)
|
||||
- **Entrypoint:** `docker-entrypoint.sh` - handles CA certs, user permissions, database init
|
||||
- **Compose files:**
|
||||
- `docker-compose.alt.yml` - Quick start (pre-built image, minimal config)
|
||||
- `docker-compose.yml` - Full setup (build from source, all env vars)
|
||||
- `docker-compose.dev.yml` - Development with hot reload
|
||||
|
||||
### Authentication Configuration
|
||||
## Additional Resources
|
||||
|
||||
#### SSO Provider Configuration
|
||||
- **issuerUrl**: OIDC issuer URL (e.g., https://accounts.google.com)
|
||||
- **domain**: Email domain for this provider
|
||||
- **providerId**: Unique identifier for the provider
|
||||
- **clientId**: OAuth client ID from provider
|
||||
- **clientSecret**: OAuth client secret from provider
|
||||
- **authorizationEndpoint**: OAuth authorization URL (auto-discovered if supported)
|
||||
- **tokenEndpoint**: OAuth token exchange URL (auto-discovered if supported)
|
||||
- **jwksEndpoint**: JSON Web Key Set URL (optional, auto-discovered)
|
||||
- **userInfoEndpoint**: User information endpoint (optional, auto-discovered)
|
||||
|
||||
#### OIDC Provider Settings (for external apps)
|
||||
- **allowedRedirectUris**: Comma-separated list of allowed redirect URIs
|
||||
- **clientId**: Generated client ID for the application
|
||||
- **clientSecret**: Generated client secret for the application
|
||||
- **scopes**: Available scopes (openid, profile, email)
|
||||
|
||||
#### Environment Variables
|
||||
- **BETTER_AUTH_SECRET**: Secret key for signing sessions (required)
|
||||
- **BETTER_AUTH_URL**: Base URL for authentication (default: http://localhost:4321)
|
||||
|
||||
## Security Guidelines
|
||||
|
||||
- **Confidentiality Guidelines**:
|
||||
- Dont ever say Claude Code or generated with AI anyhwere.
|
||||
- Never commit without the explicict ask
|
||||
- **Environment Variables:** See `docs/ENVIRONMENT_VARIABLES.md` for complete list
|
||||
- **Development Workflow:** See `docs/DEVELOPMENT_WORKFLOW.md`
|
||||
- **SSO Setup:** See `docs/SSO-OIDC-SETUP.md`
|
||||
- **Contributing:** See `CONTRIBUTING.md` for code guidelines and scope
|
||||
- **Graceful Shutdown:** See `docs/GRACEFUL_SHUTDOWN.md` for crash recovery details
|
||||
@@ -1,51 +1,104 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM oven/bun:1.2.21-alpine AS base
|
||||
FROM oven/bun:1.3.14-debian@sha256:9dba1a1b43ce28c9d7931bfc4eb00feb63b0114720a0277a8f939ae4dfc9db6f AS base
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl ca-certificates
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ gcc wget sqlite3 openssl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ----------------------------
|
||||
FROM base AS deps
|
||||
FROM base AS builder
|
||||
COPY package.json ./
|
||||
COPY bun.lock* ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# ----------------------------
|
||||
FROM deps AS builder
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
RUN mkdir -p dist/scripts && \
|
||||
for script in scripts/*.ts; do \
|
||||
bun build "$script" --target=bun --outfile=dist/scripts/$(basename "${script%.ts}.js"); \
|
||||
done
|
||||
for script in scripts/*.ts; do \
|
||||
if [ "$(basename "$script")" = "runtime-server.ts" ]; then continue; fi; \
|
||||
bun build "$script" --target=bun --outfile=dist/scripts/$(basename "${script%.ts}.js"); \
|
||||
done
|
||||
|
||||
# ----------------------------
|
||||
FROM deps AS pruner
|
||||
RUN bun install --production --frozen-lockfile
|
||||
FROM base AS pruner
|
||||
COPY package.json ./
|
||||
COPY bun.lock* ./
|
||||
RUN bun install --production --omit=peer --frozen-lockfile
|
||||
|
||||
# ----------------------------
|
||||
FROM base AS runner
|
||||
# Build git-lfs from source with patched Go to resolve Go stdlib CVEs
|
||||
FROM debian:trixie-slim AS git-lfs-builder
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates git make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ARG GO_VERSION=1.27.1
|
||||
ARG GIT_LFS_VERSION=3.7.1
|
||||
# Pinned rather than @latest: a cached layer kept an old @latest resolution
|
||||
# after golang.org/x/crypto 0.56.0 fixed CVE-2026-56855 and CVE-2026-78662.
|
||||
# x/crypto 0.56.0 needs Go 1.26 or newer, hence the toolchain above.
|
||||
ARG GO_X_CRYPTO_VERSION=v0.56.0
|
||||
ARG GO_X_NET_VERSION=v0.58.0
|
||||
RUN ARCH="$(dpkg --print-architecture)" \
|
||||
&& wget -qO /tmp/go.tar.gz "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
|
||||
&& tar -C /usr/local -xzf /tmp/go.tar.gz \
|
||||
&& rm /tmp/go.tar.gz
|
||||
ENV PATH="/usr/local/go/bin:/root/go/bin:${PATH}"
|
||||
# Force using our installed Go (not the version in go.mod toolchain directive)
|
||||
ENV GOTOOLCHAIN=local
|
||||
RUN git clone --branch "v${GIT_LFS_VERSION}" --depth 1 https://github.com/git-lfs/git-lfs.git /tmp/git-lfs \
|
||||
&& cd /tmp/git-lfs \
|
||||
&& go get "golang.org/x/crypto@${GO_X_CRYPTO_VERSION}" \
|
||||
&& go get "golang.org/x/net@${GO_X_NET_VERSION}" \
|
||||
&& go mod tidy \
|
||||
&& make \
|
||||
&& install -m 755 /tmp/git-lfs/bin/git-lfs /usr/local/bin/git-lfs
|
||||
|
||||
# ----------------------------
|
||||
FROM oven/bun:1.3.14-debian@sha256:9dba1a1b43ce28c9d7931bfc4eb00feb63b0114720a0277a8f939ae4dfc9db6f AS runner
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
git wget sqlite3 openssl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# The app runs as an unprivileged user and never calls a setuid helper.
|
||||
# Dropping the bits closes the path CVE-2026-78409 and CVE-2026-78410 need
|
||||
# in mount(8); Debian ships no trixie fix for them. See .vex/README.md.
|
||||
RUN find / -xdev -type f -perm /6000 -exec chmod a-s '{}' +
|
||||
# OpenVEX statements for CVEs this image cannot reach, so that
|
||||
# `docker scout cves` on the published image reports them as not affected.
|
||||
COPY .vex/*.vex.json /var/lib/db/
|
||||
COPY --from=git-lfs-builder /usr/local/bin/git-lfs /usr/local/bin/git-lfs
|
||||
RUN git lfs install
|
||||
COPY --from=pruner /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
COPY --from=builder /app/scripts ./scripts
|
||||
COPY --from=builder /app/scripts/runtime-server.ts ./scripts/runtime-server.ts
|
||||
COPY --from=builder /app/drizzle ./drizzle
|
||||
|
||||
# Remove build-only packages that are not needed at runtime
|
||||
# (esbuild, vite, rollup, tailwind, svgo — all only used during `astro build`)
|
||||
RUN rm -rf node_modules/esbuild node_modules/@esbuild \
|
||||
node_modules/rollup node_modules/@rollup \
|
||||
node_modules/vite node_modules/svgo \
|
||||
node_modules/@tailwindcss/vite \
|
||||
node_modules/tailwindcss
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
ENV DATABASE_URL=file:data/gitea-mirror.db
|
||||
ENV BASE_URL=/
|
||||
|
||||
# Create directories and setup permissions
|
||||
RUN mkdir -p /app/certs && \
|
||||
chmod +x ./docker-entrypoint.sh && \
|
||||
mkdir -p /app/data && \
|
||||
addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 gitea-mirror && \
|
||||
chown -R gitea-mirror:nodejs /app/data && \
|
||||
chown -R gitea-mirror:nodejs /app/certs
|
||||
chmod +x ./docker-entrypoint.sh && \
|
||||
mkdir -p /app/data && \
|
||||
groupadd --system --gid 1001 nodejs && \
|
||||
useradd --system --uid 1001 --gid 1001 --create-home --home-dir /home/gitea-mirror gitea-mirror && \
|
||||
chown -R gitea-mirror:nodejs /app/data && \
|
||||
chown -R gitea-mirror:nodejs /app/certs && \
|
||||
chown -R gitea-mirror:nodejs /home/gitea-mirror
|
||||
|
||||
USER gitea-mirror
|
||||
|
||||
@@ -53,6 +106,6 @@ VOLUME /app/data
|
||||
EXPOSE 4321
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:4321/api/health || exit 1
|
||||
CMD sh -c 'BASE="${BASE_URL:-/}"; if [ "$BASE" = "/" ]; then BASE=""; else BASE="${BASE%/}"; fi; wget --no-verbose --tries=1 --spider "http://localhost:4321${BASE}/api/health" || exit 1'
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
@@ -0,0 +1,189 @@
|
||||
# Nix Deployment Quick Reference
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
# From GitHub (no clone needed!)
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Or from local clone
|
||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
||||
```
|
||||
|
||||
Secrets auto-generate, database auto-initializes, and the web UI starts at http://localhost:4321.
|
||||
|
||||
**Note:** If you have flakes enabled in your nix config, you can omit `--extra-experimental-features 'nix-command flakes'`
|
||||
|
||||
---
|
||||
|
||||
## Installation Options
|
||||
|
||||
### 1. Run Without Installing (from GitHub)
|
||||
```bash
|
||||
# Latest version from main branch
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Pin to specific version
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
```
|
||||
|
||||
### 2. Install to Profile
|
||||
```bash
|
||||
# Install from GitHub
|
||||
nix profile install --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Run the installed binary
|
||||
gitea-mirror
|
||||
```
|
||||
|
||||
### 3. Use Local Clone
|
||||
```bash
|
||||
# Clone and run
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
||||
```
|
||||
|
||||
### 4. NixOS System Service
|
||||
```nix
|
||||
# configuration.nix
|
||||
{
|
||||
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
|
||||
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
betterAuthUrl = "https://mirror.example.com"; # For production
|
||||
openFirewall = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Development (Local Clone)
|
||||
```bash
|
||||
nix develop --extra-experimental-features 'nix-command flakes'
|
||||
# or
|
||||
direnv allow # Handles experimental features automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enable Flakes Permanently (Recommended)
|
||||
|
||||
To avoid typing `--extra-experimental-features` every time, add to `~/.config/nix/nix.conf`:
|
||||
```
|
||||
experimental-features = nix-command flakes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Gets Auto-Generated?
|
||||
|
||||
On first run, the wrapper automatically:
|
||||
|
||||
1. Creates `~/.local/share/gitea-mirror/` (or `$DATA_DIR`)
|
||||
2. Generates `BETTER_AUTH_SECRET` → `.better_auth_secret`
|
||||
3. Generates `ENCRYPTION_SECRET` → `.encryption_secret`
|
||||
4. Initializes SQLite database
|
||||
5. Runs startup recovery and repair scripts
|
||||
6. Starts the application
|
||||
|
||||
---
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
# Database management
|
||||
gitea-mirror-db init # Initialize database
|
||||
gitea-mirror-db check # Health check
|
||||
gitea-mirror-db fix # Fix issues
|
||||
|
||||
# Development (add --extra-experimental-features 'nix-command flakes' if needed)
|
||||
nix develop # Enter dev shell
|
||||
nix build # Build package
|
||||
nix flake check # Validate flake
|
||||
nix flake update # Update dependencies
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All vars from `docker-compose.alt.yml` are supported:
|
||||
|
||||
```bash
|
||||
DATA_DIR="$HOME/.local/share/gitea-mirror"
|
||||
PORT=4321
|
||||
HOST="0.0.0.0"
|
||||
BETTER_AUTH_URL="http://localhost:4321"
|
||||
|
||||
# Secrets (auto-generated if not set)
|
||||
BETTER_AUTH_SECRET=auto-generated
|
||||
ENCRYPTION_SECRET=auto-generated
|
||||
|
||||
# Concurrency (for perfect ordering, set both to 1)
|
||||
MIRROR_ISSUE_CONCURRENCY=3
|
||||
MIRROR_PULL_REQUEST_CONCURRENCY=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NixOS Module Options
|
||||
|
||||
```nix
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
package = ...; # Override package
|
||||
dataDir = "/var/lib/gitea-mirror"; # Data location
|
||||
user = "gitea-mirror"; # Service user
|
||||
group = "gitea-mirror"; # Service group
|
||||
host = "0.0.0.0"; # Bind address
|
||||
port = 4321; # Listen port
|
||||
betterAuthUrl = "http://..."; # External URL
|
||||
betterAuthTrustedOrigins = "..."; # CORS origins
|
||||
mirrorIssueConcurrency = 3; # Concurrency
|
||||
mirrorPullRequestConcurrency = 5; # Concurrency
|
||||
environmentFile = null; # Optional secrets file
|
||||
openFirewall = true; # Open firewall
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Docker vs Nix
|
||||
|
||||
| Feature | Docker | Nix |
|
||||
|---------|--------|-----|
|
||||
| **Config Required** | BETTER_AUTH_SECRET | None (auto-generated) |
|
||||
| **Startup** | `docker-compose up` | `nix run .#gitea-mirror` |
|
||||
| **Service** | Docker daemon | systemd (NixOS) |
|
||||
| **Updates** | `docker pull` | `nix flake update` |
|
||||
| **Reproducible** | Image-based | Hash-based |
|
||||
|
||||
---
|
||||
|
||||
## Full Documentation
|
||||
|
||||
- **[docs/NIX_DEPLOYMENT.md](docs/NIX_DEPLOYMENT.md)** - Complete deployment guide
|
||||
- NixOS module configuration
|
||||
- Home Manager integration
|
||||
- Production deployment examples
|
||||
- Migration from Docker
|
||||
- Troubleshooting guide
|
||||
|
||||
- **[docs/NIX_DISTRIBUTION.md](docs/NIX_DISTRIBUTION.md)** - Distribution guide for maintainers
|
||||
- How users consume the package
|
||||
- CI build caching
|
||||
- Releasing new versions
|
||||
- Submitting to nixpkgs
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Zero-config deployment** - Runs immediately without setup
|
||||
- **Auto-secret generation** - Secure secrets created and persisted
|
||||
- **Startup recovery** - Handles interrupted jobs automatically
|
||||
- **Graceful shutdown** - Proper signal handling
|
||||
- **Health checks** - Built-in monitoring support
|
||||
- **Security hardening** - NixOS module includes systemd protections
|
||||
- **Docker parity** - Same behavior as `docker-compose.alt.yml`
|
||||
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<img src=".github/assets/logo.png" alt="Gitea Mirror Logo" width="120" />
|
||||
<h1>Gitea Mirror</h1>
|
||||
<p><i>Automatically mirror repositories from GitHub to your self-hosted Gitea instance.</i></p>
|
||||
<p><i>Automatically mirror repositories from GitHub to your self-hosted Gitea/Forgejo instance.</i></p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/RayLabsHQ/gitea-mirror?label=release" alt="release"/></a>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/RayLabsHQ/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
||||
@@ -10,10 +10,6 @@
|
||||
</p>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Upgrading to v3?** v3 requires a fresh start with a new data volume. Please read the [Upgrade Guide](UPGRADE.md) for instructions.
|
||||
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
@@ -23,7 +19,9 @@ docker compose -f docker-compose.alt.yml up -d
|
||||
# Access at http://localhost:4321
|
||||
```
|
||||
|
||||
First user signup becomes admin. Configure GitHub and Gitea through the web interface!
|
||||
First user signup becomes admin. Configure GitHub and Gitea/Forgejo through the web interface!
|
||||
|
||||
📖 **[Full documentation](https://gitea-mirror.raylabs.io/docs/)** - quickstart, deployment, configuration, notifications, SSO and more. Offline copies live in [`docs/`](docs/).
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/assets/dashboard.png" alt="Dashboard" width="600" />
|
||||
@@ -32,18 +30,23 @@ First user signup becomes admin. Configure GitHub and Gitea through the web inte
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🔁 Mirror public, private, and starred GitHub repos to Gitea
|
||||
- 🏢 Mirror entire organizations with flexible strategies
|
||||
- 🔁 Mirror public, private, and starred GitHub repos to Gitea/Forgejo
|
||||
- 🦊 **Multiple sources (GitLab and Gitea/Forgejo beta)** - Connect several sources at once: GitHub and GitHub Enterprise, GitLab, Gitea/Forgejo, and each repository keeps its source. gitlab.com, Codeberg and self hosted instances mirror code, tags, wiki and LFS (issues, pull requests and releases need a GitHub source). GitHub to Gitea is the supported default; every other source or destination is beta. See [docs/SOURCE_PROVIDERS.md](docs/SOURCE_PROVIDERS.md)
|
||||
- 🏛️ **GitHub Enterprise support** - Works with GHES and GHEC with data residency via `GH_API_URL`
|
||||
- 🚀 **Forgejo, GitHub and GitLab destinations (beta)** - Forgejo works like Gitea. Pick GitHub or GitLab and the app keeps a bare clone of each repository and pushes its branches and tags there. Code only, and the target is overwritten on every sync. See [docs/PUSH_TARGETS.md](docs/PUSH_TARGETS.md)
|
||||
- 🏢 Mirror entire organizations with flexible strategies, including public organizations from GitHub, GitLab or Gitea/Forgejo with no source account configured
|
||||
- 🎯 Custom destination control for repos and organizations
|
||||
- 📦 **Git LFS support** - Mirror large files with Git LFS
|
||||
- 📝 **Metadata mirroring** - Issues, pull requests (as issues), labels, milestones, wiki
|
||||
- 📝 **Metadata mirroring** - Issues, pull requests (as issues), labels, milestones, releases, wiki
|
||||
- 🚫 **Repository ignore** - Mark specific repos to skip
|
||||
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC)
|
||||
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC, header auth)
|
||||
- 🔔 **[Notifications](docs/NOTIFICATIONS.md)** - ntfy, Apprise, Gotify, or any webhook with signed payloads
|
||||
- 📊 Real-time dashboard with activity logs
|
||||
- ⏱️ Scheduled automatic mirroring with configurable intervals
|
||||
- 🔄 **Auto-discovery** - Automatically import new GitHub repositories (v3.4.0+)
|
||||
- 🧹 **Repository cleanup** - Auto-remove repos deleted from GitHub (v3.4.0+)
|
||||
- 🎯 **Proper mirror intervals** - Respects configured sync intervals (v3.4.0+)
|
||||
- 🔄 **Auto-discovery** - Automatically import new GitHub repositories
|
||||
- 🧹 **Repository cleanup** - Auto-remove repos deleted from GitHub
|
||||
- 🎯 **Proper mirror intervals** - Respects configured sync intervals
|
||||
- 🛡️ **[Force-push protection](docs/FORCE_PUSH_PROTECTION.md)** - Smart detection with backup-on-demand or block-and-approve modes
|
||||
- 🗑️ Automatic database cleanup with configurable retention
|
||||
- 🐳 Dockerized with multi-arch support (AMD64/ARM64)
|
||||
|
||||
@@ -51,7 +54,7 @@ First user signup becomes admin. Configure GitHub and Gitea through the web inte
|
||||
|
||||
<div align="center">
|
||||
<img src=".github/assets/repositories.png" alt="Repositories" width="600" />
|
||||
<img src=".github/assets/repositories_mobile.png" alt="Rrepositories Mobile" width="200" />
|
||||
<img src=".github/assets/repositories_mobile.png" alt="Repositories Mobile" width="200" />
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
@@ -116,7 +119,7 @@ docker compose up -d
|
||||
#### Using Pre-built Image Directly
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/raylabshq/gitea-mirror:v3.1.1
|
||||
docker pull ghcr.io/raylabshq/gitea-mirror:latest
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
@@ -154,6 +157,38 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Proxmo
|
||||
|
||||
See the [Proxmox VE Community Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror) for more details.
|
||||
|
||||
### Nix/NixOS
|
||||
|
||||
Zero-configuration deployment with Nix:
|
||||
|
||||
```bash
|
||||
# Run immediately - no setup needed!
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Or build and run locally
|
||||
nix build --extra-experimental-features 'nix-command flakes'
|
||||
./result/bin/gitea-mirror
|
||||
|
||||
# Or install to profile
|
||||
nix profile install --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
gitea-mirror
|
||||
```
|
||||
|
||||
**NixOS users** - add to your configuration:
|
||||
```nix
|
||||
{
|
||||
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
|
||||
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
betterAuthUrl = "https://mirror.example.com";
|
||||
openFirewall = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Secrets auto-generate, database auto-initializes. See [NIX.md](NIX.md) for quick reference or [docs/NIX_DEPLOYMENT.md](docs/NIX_DEPLOYMENT.md) for full documentation.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
```bash
|
||||
@@ -170,42 +205,46 @@ bun run dev
|
||||
1. **First Time Setup**
|
||||
- Navigate to http://localhost:4321
|
||||
- Create admin account (first user signup)
|
||||
- Configure GitHub and Gitea connections
|
||||
- Configure the source and destination connections. GitHub to Gitea is the default. Forgejo works like Gitea, and GitHub or GitLab as destination uses push targets. Everything other than GitHub to Gitea is beta
|
||||
|
||||
2. **Mirror Strategies**
|
||||
- **Preserve Structure**: Maintains GitHub organization structure
|
||||
- **Single Organization**: All repos go to one Gitea organization
|
||||
- **Flat User**: All repos under your Gitea user account
|
||||
- **Single Organization**: All repos go to one Gitea/Forgejo organization
|
||||
- **Flat User**: All repos under your Gitea/Forgejo user account
|
||||
- **Mixed Mode**: Personal repos in one org, organization repos preserve structure
|
||||
|
||||
3. **Customization**
|
||||
- Click edit buttons on organization cards to set custom destinations
|
||||
- Override individual repository destinations in the table view
|
||||
- Starred repositories automatically go to a dedicated organization
|
||||
- Starred repositories can go to a dedicated org or preserve source owner/org paths
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Git LFS (Large File Storage)
|
||||
Mirror Git LFS objects along with your repositories:
|
||||
- Enable "Mirror LFS" option in Settings → Mirror Options
|
||||
- Requires Gitea server with LFS enabled (`LFS_START_SERVER = true`)
|
||||
- Requires Gitea/Forgejo server with LFS enabled (`LFS_START_SERVER = true`)
|
||||
- Requires Git v2.1.2+ on the server
|
||||
|
||||
### Metadata Mirroring
|
||||
Transfer complete repository metadata from GitHub to Gitea:
|
||||
Transfer complete repository metadata from GitHub to Gitea/Forgejo:
|
||||
- **Issues** - Mirror all issues with comments and labels
|
||||
- **Pull Requests** - Transfer PR discussions to Gitea
|
||||
- **Pull Requests** - Transfer PR discussions to Gitea/Forgejo
|
||||
- **Labels** - Preserve repository labels
|
||||
- **Milestones** - Keep project milestones
|
||||
- **Wiki** - Mirror wiki content
|
||||
- **Releases** - Transfer GitHub releases with assets
|
||||
|
||||
Releases have two limits. The release limit keeps only the newest N releases in Gitea/Forgejo. The release asset limit uploads assets only for the newest N of those, so a repository with years of releases can keep every release note and tag while only the recent binaries take up space. Leave the asset limit empty to upload assets for every mirrored release, or set it to 0 for release notes only. Both limits can be overridden per organization and per repository, and lowering the asset limit never deletes assets that were already uploaded.
|
||||
|
||||
Enable in Settings → Mirror Options → Mirror metadata
|
||||
|
||||
### Repository Management
|
||||
- **Ignore Status** - Mark repositories to skip from mirroring
|
||||
- **Automatic Cleanup** - Configure retention period for activity logs
|
||||
- **Scheduled Sync** - Set custom intervals for automatic mirroring
|
||||
- **Reconcile with Destination** - Compare Gitea/Forgejo with the database from Configuration > Automation: adopt mirrors the database lost track of, record the new owner of mirrors you transferred in Gitea, or reset rows whose mirror is gone so the next sync recreates them. Nothing is deleted or archived by this step.
|
||||
- **Move Mirrors from the App** - Changing a repository's or an organization's destination offers to transfer the existing mirrors on Gitea/Forgejo as well, with their history, issues and mirror settings. When the token cannot create repositories under the new owner, Gitea asks its owners to accept and sync follows the mirror once they do.
|
||||
|
||||
### Automatic Syncing & Synchronization
|
||||
|
||||
@@ -214,7 +253,7 @@ Gitea Mirror provides powerful automatic synchronization features:
|
||||
#### Features (v3.4.0+)
|
||||
- **Auto-discovery**: Automatically discovers and imports new GitHub repositories
|
||||
- **Repository cleanup**: Removes repositories that no longer exist in GitHub
|
||||
- **Proper intervals**: Mirrors respect your configured sync intervals (not Gitea's default 24h)
|
||||
- **Proper intervals**: Mirrors respect your configured sync intervals (not Gitea/Forgejo's default 24h)
|
||||
- **Smart scheduling**: Only syncs repositories that need updating
|
||||
- **Auto-start on boot** (v3.5.3+): Automatically imports and mirrors all repositories when `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set - no manual clicks required!
|
||||
|
||||
@@ -225,7 +264,7 @@ Navigate to the Configuration page and enable "Automatic Syncing" with your pref
|
||||
|
||||
**🚀 Set it and forget it!** With these environment variables, Gitea Mirror will automatically:
|
||||
1. **Import** all your GitHub repositories on startup (no manual import needed!)
|
||||
2. **Mirror** them to Gitea immediately
|
||||
2. **Mirror** them to Gitea/Forgejo immediately
|
||||
3. **Keep them synchronized** based on your interval
|
||||
4. **Auto-discover** new repos you create/star on GitHub
|
||||
5. **Clean up** repos you delete from GitHub
|
||||
@@ -254,21 +293,95 @@ CLEANUP_DRY_RUN=false # Set to true to test without changes
|
||||
**Important Notes**:
|
||||
- **Auto-Start**: When `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set, the service automatically imports all GitHub repositories and mirrors them on startup. No manual "Import" or "Mirror" button clicks required!
|
||||
- The scheduler checks every minute for tasks to run. The `GITEA_MIRROR_INTERVAL` determines how often each repository is actually synced. For example, with `8h`, each repo syncs every 8 hours from its last successful sync.
|
||||
- **Large repo bootstrap**: For first-time mirroring of large repositories (especially with metadata/LFS), avoid very short intervals (for example `5m`). Start with a longer interval (`1h` to `8h`) or temporarily disable scheduling during the initial import/mirror run, then enable your regular interval after the first pass completes.
|
||||
- **Why this matters**: If your Gitea/Forgejo instance takes a long time to complete migrations/imports, aggressive schedules can cause repeated retries and duplicate-looking mirror attempts.
|
||||
|
||||
**🛡️ Backup Protection Features**:
|
||||
- **No Accidental Deletions**: Repository cleanup is automatically skipped if GitHub is inaccessible (account deleted, banned, or API errors)
|
||||
- **Archive Never Deletes Data**: The `archive` action preserves all repository data:
|
||||
- Regular repositories: Made read-only using Gitea's archive feature
|
||||
- Mirror repositories: Renamed with `[ARCHIVED]` prefix (Gitea API limitation prevents archiving mirrors)
|
||||
- Regular repositories: Made read-only using Gitea/Forgejo's archive feature
|
||||
- Mirror repositories: Renamed with `archived-` prefix (Gitea/Forgejo API limitation prevents archiving mirrors)
|
||||
- Failed operations: Repository remains fully accessible even if marking as archived fails
|
||||
- **The Whole Point of Backups**: Your Gitea mirrors are preserved even when GitHub sources disappear - that's why you have backups!
|
||||
- **Manual Sync on Demand**: Archived mirrors stay in Gitea/Forgejo with automatic syncs disabled; trigger `Manual Sync` from the Repositories page whenever you need fresh data.
|
||||
- **The Whole Point of Backups**: Your Gitea/Forgejo mirrors are preserved even when GitHub sources disappear - that's why you have backups!
|
||||
- **Strongly Recommended**: Always use `CLEANUP_ORPHANED_REPO_ACTION=archive` (default) instead of `delete`
|
||||
|
||||
### GitHub Enterprise (GHES / GHEC with Data Residency)
|
||||
|
||||
Gitea Mirror works with non-`github.com` GitHub deployments. Point the client at your Enterprise API via the `GH_API_URL` environment variable:
|
||||
|
||||
```bash
|
||||
# GitHub Enterprise Server (self-hosted)
|
||||
GH_API_URL=https://ghe.example.com/api/v3
|
||||
|
||||
# GitHub Enterprise Cloud with data residency
|
||||
GH_API_URL=https://api.TENANT.ghe.com
|
||||
```
|
||||
|
||||
Standard GitHub Enterprise Cloud on `github.com` needs no override. Use a token issued by the target Enterprise instance for `GITHUB_TOKEN`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Reverse Proxy Configuration
|
||||
|
||||
If using a reverse proxy (e.g., nginx proxy manager) and experiencing issues with JavaScript files not loading properly, try enabling HTTP/2 support in your proxy configuration. While not required by the application, some proxy configurations may have better compatibility with HTTP/2 enabled. See [issue #43](https://github.com/RayLabsHQ/gitea-mirror/issues/43) for reference.
|
||||
If you run behind a reverse proxy on a subpath (for example `https://git.example.com/mirror`), configure:
|
||||
|
||||
```bash
|
||||
# BASE_URL handles the path prefix — auth URLs stay as origin only
|
||||
BASE_URL=/mirror
|
||||
BETTER_AUTH_URL=https://git.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# → Auth endpoints resolve to: https://git.example.com/mirror/api/auth/*
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `BASE_URL` sets the application path prefix.
|
||||
- `BETTER_AUTH_URL` and `PUBLIC_BETTER_AUTH_URL` should be **origin only** (e.g. `https://git.example.com`). Do not include the base path — it is applied automatically from `BASE_URL`. Any path accidentally included is stripped.
|
||||
- `BETTER_AUTH_TRUSTED_ORIGINS` should also contain origins only (no path).
|
||||
- `BASE_URL` is runtime configuration, so prebuilt registry images can be reused across different subpaths.
|
||||
|
||||
### Mirror Token Rotation (GitHub Token Changed)
|
||||
|
||||
For existing pull-mirror repositories, changing the GitHub token in Gitea Mirror does not always update stored mirror credentials in Gitea/Forgejo for already-created repositories.
|
||||
|
||||
If sync logs show authentication failures (for example `terminal prompts disabled`), do one of the following:
|
||||
|
||||
1. In Gitea/Forgejo, open repository **Settings → Mirror Settings** and update the mirror authorization password/token.
|
||||
2. Or delete and re-mirror the repository so it is recreated with current credentials.
|
||||
|
||||
### Re-sync Metadata After Changing Mirror Options
|
||||
|
||||
If you enable metadata options (issues/PRs/labels/milestones/releases) after repositories were already mirrored:
|
||||
|
||||
1. Go to **Repositories**, select the repositories, and click **Sync** to run a fresh sync pass.
|
||||
2. For a full metadata refresh, use **Re-run Metadata** on selected repositories. This clears metadata sync state for those repos and immediately starts Sync.
|
||||
3. If some repositories still miss metadata, reset metadata sync state in SQLite and sync again:
|
||||
|
||||
```bash
|
||||
sqlite3 data/gitea-mirror.db "UPDATE repositories SET metadata = NULL;"
|
||||
```
|
||||
|
||||
This clears per-repository metadata completion flags so the next sync can re-run metadata import steps.
|
||||
|
||||
### Mirror Interval vs Gitea/Forgejo `MIN_INTERVAL`
|
||||
|
||||
Gitea Mirror treats the interval configured in **Configuration** (or `GITEA_MIRROR_INTERVAL`) as the source of truth and applies it to mirrored repositories during sync.
|
||||
|
||||
If your Gitea/Forgejo server has `mirror.MIN_INTERVAL` set to a higher value (for example `24h`) and Gitea Mirror is set lower (for example `8h`), sync/mirror operations can fail when updating mirror settings.
|
||||
|
||||
To avoid this:
|
||||
|
||||
1. Set Gitea Mirror interval to a value greater than or equal to your server `MIN_INTERVAL`.
|
||||
2. Do not rely on manual per-repository mirror interval edits in Gitea/Forgejo, as they will be overwritten on sync.
|
||||
|
||||
### Pushing to a mirror fails with `mirror repository is read-only`
|
||||
|
||||
Every repository this tool creates is a Gitea/Forgejo pull mirror, and pull mirrors are read-only by design. Gitea/Forgejo refuses pushes to them so the copy can never drift from the source.
|
||||
|
||||
Make your commits in the source repository (on GitHub, GitLab or wherever the mirror pulls from). They arrive in the mirror on the next sync. Gitea Mirror is a backup tool and does not push changes back to the source.
|
||||
|
||||
If you want a repository you can push to, clone the mirror, push the clone to a normal (non-mirror) repository, and stop mirroring the original.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -290,13 +403,13 @@ bun run build
|
||||
|
||||
- **Frontend**: Astro, React, Shadcn UI, Tailwind CSS v4
|
||||
- **Backend**: Bun runtime, SQLite, Drizzle ORM
|
||||
- **APIs**: GitHub (Octokit), Gitea REST API
|
||||
- **APIs**: GitHub (Octokit), Gitea/Forgejo REST API
|
||||
- **Auth**: Better Auth with session-based authentication
|
||||
|
||||
## Security
|
||||
|
||||
### Token Encryption
|
||||
- All GitHub and Gitea API tokens are encrypted at rest using AES-256-GCM
|
||||
- All GitHub and Gitea/Forgejo API tokens are encrypted at rest using AES-256-GCM
|
||||
- Encryption is automatic and transparent to users
|
||||
- Set `ENCRYPTION_SECRET` environment variable for production deployments
|
||||
- Falls back to `BETTER_AUTH_SECRET` if not set
|
||||
@@ -306,6 +419,20 @@ bun run build
|
||||
- Never stored in plaintext
|
||||
- Secure cookie-based session management
|
||||
|
||||
### Admin Password Recovery (CLI)
|
||||
If email delivery is not configured, an admin with server access can reset a user password from the command line:
|
||||
|
||||
```bash
|
||||
bun run reset-password -- --email=user@example.com --new-password='new-secure-password'
|
||||
```
|
||||
|
||||
What this does:
|
||||
- Updates the credential password hash for the matching user
|
||||
- Creates a credential account if one does not already exist
|
||||
- Invalidates all active sessions for that user (forces re-login)
|
||||
|
||||
Use this only from trusted server/admin environments.
|
||||
|
||||
## Authentication
|
||||
|
||||
Gitea Mirror supports multiple authentication methods. **Email/password authentication is the default and always enabled.**
|
||||
@@ -329,6 +456,8 @@ Enable users to sign in with external identity providers like Google, Azure AD,
|
||||
https://your-domain.com/api/auth/sso/callback/{provider-id}
|
||||
```
|
||||
|
||||
Need help? The [SSO & OIDC guide](docs/SSO-OIDC-SETUP.md) now includes a working Authentik walkthrough plus troubleshooting tips. If you upgraded from a version earlier than v3.8.10 and see `TypeError … url.startsWith` after the callback, delete the old provider and add it again using the Discover button (see [#73](https://github.com/RayLabsHQ/gitea-mirror/issues/73) and [#122](https://github.com/RayLabsHQ/gitea-mirror/issues/122)).
|
||||
|
||||
### 3. Header Authentication (Reverse Proxy)
|
||||
Perfect for automatic authentication when using reverse proxies like Authentik, Authelia, or Traefik Forward Auth.
|
||||
|
||||
@@ -371,16 +500,28 @@ Gitea Mirror can also act as an OIDC provider for other applications. Register O
|
||||
- Create service-to-service authentication
|
||||
- Build integrations with your Gitea Mirror instance
|
||||
|
||||
### 5. API Keys (Scripts and CI)
|
||||
Create a personal API key under Settings → Authentication → API Keys and send it in the `x-api-key` header to call the app from scripts, CI pipelines or workflow tools. Keys act as the user who created them, can be given an expiry, and can be revoked at any time.
|
||||
|
||||
```bash
|
||||
curl -sS -X POST https://your-domain.com/api/sync/repository \
|
||||
-H "x-api-key: gm_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"owner":"octocat","repo":"hello-world"}'
|
||||
```
|
||||
|
||||
See the [API guide](docs/API.md) for the endpoints to add, list and mirror repositories.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Pull Request Mirroring Implementation
|
||||
Pull requests **cannot be created as actual PRs** in Gitea due to API limitations. Instead, they are mirrored as **enriched issues** with comprehensive metadata.
|
||||
Pull requests **cannot be created as actual PRs** in Gitea/Forgejo due to API limitations. Instead, they are mirrored as **enriched issues** with comprehensive metadata.
|
||||
|
||||
**Why real PR mirroring isn't possible:**
|
||||
- Gitea's API doesn't support creating pull requests from external sources
|
||||
- Gitea/Forgejo's API doesn't support creating pull requests from external sources
|
||||
- Real PRs require actual Git branches with commits to exist in the repository
|
||||
- Would require complex branch synchronization and commit replication
|
||||
- The mirror relationship is one-way (GitHub → Gitea) for repository content
|
||||
- The mirror relationship is one-way (GitHub → Gitea/Forgejo) for repository content
|
||||
|
||||
**How we handle Pull Requests:**
|
||||
PRs are mirrored as issues with rich metadata including:
|
||||
@@ -394,7 +535,7 @@ PRs are mirrored as issues with rich metadata including:
|
||||
- 🔀 Base and head branch information
|
||||
- ✅ Merge status tracking
|
||||
|
||||
This approach preserves all important PR information while working within Gitea's API constraints. The PRs appear in Gitea's issue tracker with clear visual distinction and comprehensive details.
|
||||
This approach preserves all important PR information while working within Gitea/Forgejo's API constraints. The PRs appear in the issue tracker with clear visual distinction and comprehensive details.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -402,22 +543,23 @@ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTIN
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.
|
||||
GNU Affero General Public License v3.0 (AGPL-3.0) - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#RayLabsHQ/gitea-mirror&Date">
|
||||
<a href="https://www.star-history.com/?repos=RayLabsHQ%2Fgitea-mirror&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=RayLabsHQ/gitea-mirror&type=date&theme=dark&legend=top-left&sealed_token=zodo-E2HH58BX1LXhjYFF1dKmophMxE1C-ho3bvwiiC1iFxC7MTeKTy-uQgcInXqQV6QF5oHXYjDoHhPdUOJNnlgjYmVqsCMTvLVrcFTD0DWKH6fvG6qC-xfXS_2bUWPt_ybnIsuoOoTWppmkCLlfTy0-_jpkHQOrTorh3Hbul43dUWUwJraXc2W6XHD" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=RayLabsHQ/gitea-mirror&type=date&legend=top-left&sealed_token=zodo-E2HH58BX1LXhjYFF1dKmophMxE1C-ho3bvwiiC1iFxC7MTeKTy-uQgcInXqQV6QF5oHXYjDoHhPdUOJNnlgjYmVqsCMTvLVrcFTD0DWKH6fvG6qC-xfXS_2bUWPt_ybnIsuoOoTWppmkCLlfTy0-_jpkHQOrTorh3Hbul43dUWUwJraXc2W6XHD" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=RayLabsHQ/gitea-mirror&type=date&legend=top-left&sealed_token=zodo-E2HH58BX1LXhjYFF1dKmophMxE1C-ho3bvwiiC1iFxC7MTeKTy-uQgcInXqQV6QF5oHXYjDoHhPdUOJNnlgjYmVqsCMTvLVrcFTD0DWKH6fvG6qC-xfXS_2bUWPt_ybnIsuoOoTWppmkCLlfTy0-_jpkHQOrTorh3Hbul43dUWUwJraXc2W6XHD" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://github.com/RayLabsHQ/gitea-mirror/tree/main/docs)
|
||||
- 🔐 [Custom CA Certificates](docs/CA_CERTIFICATES.md)
|
||||
- 🔐 [Environment Variables](docs/ENVIRONMENT_VARIABLES.md)
|
||||
- 🛡️ [Force-Push Protection](docs/FORCE_PUSH_PROTECTION.md)
|
||||
- 🐛 [Report Issues](https://github.com/RayLabsHQ/gitea-mirror/issues)
|
||||
- 💬 [Discussions](https://github.com/RayLabsHQ/gitea-mirror/discussions)
|
||||
- 🔧 [Proxmox VE Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror)
|
||||
@@ -1,74 +0,0 @@
|
||||
# Upgrade Guide
|
||||
|
||||
## Upgrading to v3.0
|
||||
|
||||
> **⚠️ IMPORTANT**: v3.0 requires a fresh start. There is no automated migration from v2.x to v3.0.
|
||||
|
||||
### Why No Migration?
|
||||
|
||||
v3.0 introduces fundamental changes to the application architecture:
|
||||
- **Authentication**: Switched from JWT to Better Auth
|
||||
- **Database**: Now uses Drizzle ORM with proper migrations
|
||||
- **Security**: All tokens are now encrypted
|
||||
- **Features**: Added SSO support and OIDC provider functionality
|
||||
|
||||
Due to these extensive changes, we recommend starting fresh with v3.0 for the best experience.
|
||||
|
||||
### Upgrade Steps
|
||||
|
||||
1. **Stop your v2.x container**
|
||||
```bash
|
||||
docker stop gitea-mirror
|
||||
docker rm gitea-mirror
|
||||
```
|
||||
|
||||
2. **Backup your v2.x data (optional)**
|
||||
```bash
|
||||
# If you want to keep your v2 data for reference
|
||||
docker run --rm -v gitea-mirror-data:/data -v $(pwd):/backup alpine tar czf /backup/gitea-mirror-v2-backup.tar.gz -C /data .
|
||||
```
|
||||
|
||||
3. **Create a new volume for v3**
|
||||
```bash
|
||||
docker volume create gitea-mirror-v3-data
|
||||
```
|
||||
|
||||
4. **Run v3 with the new volume**
|
||||
```bash
|
||||
docker run -d \
|
||||
--name gitea-mirror \
|
||||
-p 4321:4321 \
|
||||
-v gitea-mirror-v3-data:/app/data \
|
||||
-e BETTER_AUTH_SECRET=your-secret-key \
|
||||
-e ENCRYPTION_SECRET=your-encryption-key \
|
||||
arunavo4/gitea-mirror:latest
|
||||
```
|
||||
|
||||
5. **Set up your configuration again**
|
||||
- Navigate to http://localhost:4321
|
||||
- Create a new admin account
|
||||
- Re-enter your GitHub and Gitea credentials
|
||||
- Configure your mirror settings
|
||||
|
||||
### What Happens to My Existing Mirrors?
|
||||
|
||||
Your existing mirrors in Gitea are **not affected**. The application will:
|
||||
- Recognize existing repositories when you re-import
|
||||
- Skip creating duplicates
|
||||
- Resume normal mirror operations
|
||||
|
||||
### Environment Variable Changes
|
||||
|
||||
v3.0 uses different environment variables:
|
||||
|
||||
| v2.x | v3.0 | Notes |
|
||||
|------|------|-------|
|
||||
| `JWT_SECRET` | `BETTER_AUTH_SECRET` | Required for session management |
|
||||
| - | `ENCRYPTION_SECRET` | New - required for token encryption |
|
||||
|
||||
### Need Help?
|
||||
|
||||
If you have questions about upgrading:
|
||||
1. Check the [README](README.md) for v3 setup instructions
|
||||
2. Review your v2 configuration before upgrading
|
||||
3. Open an issue if you encounter problems
|
||||
@@ -14,9 +14,9 @@ export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['bun']
|
||||
}
|
||||
}
|
||||
external: ['bun', 'bun:*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: [react()]
|
||||
});
|
||||
});
|
||||
@@ -3,4 +3,7 @@
|
||||
timeout = 5000
|
||||
|
||||
# Preload the setup file
|
||||
preload = ["./src/tests/setup.bun.ts"]
|
||||
preload = ["./src/tests/setup.bun.ts"]
|
||||
|
||||
# Only run tests in src/ directory (excludes tests/e2e/ which are Playwright tests)
|
||||
root = "./src/"
|
||||
@@ -16,6 +16,13 @@ services:
|
||||
# === ABSOLUTELY REQUIRED ===
|
||||
# This MUST be set and CANNOT be changed via UI
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} # Min 32 chars, required for sessions
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:4321}
|
||||
# REVERSE PROXY: If accessing via a reverse proxy, set all three to your external URL:
|
||||
# BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
# Path-prefix deployments (e.g. /mirror) are supported at runtime via BASE_URL.
|
||||
|
||||
# === CORE SETTINGS ===
|
||||
# These are technically required but have working defaults
|
||||
@@ -23,10 +30,19 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- PUBLIC_BETTER_AUTH_URL=${PUBLIC_BETTER_AUTH_URL:-http://localhost:4321}
|
||||
# Optional concurrency controls (defaults match in-app defaults)
|
||||
# If you want perfect ordering of issues and PRs, set these at 1
|
||||
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=3 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -52,4 +68,4 @@ services:
|
||||
# - Auto-import settings
|
||||
# - Cleanup preferences
|
||||
#
|
||||
# That's it! Everything else can be configured via the web interface.
|
||||
# That's it! Everything else can be configured via the web interface.
|
||||
@@ -66,6 +66,7 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- BETTER_AUTH_SECRET=dev-secret-key
|
||||
# GitHub/Gitea Mirror Config
|
||||
- GITHUB_USERNAME=${GITHUB_USERNAME:-your-github-username}
|
||||
@@ -89,7 +90,11 @@ services:
|
||||
# Optional: Skip TLS verification (insecure, use only for testing)
|
||||
# - GITEA_SKIP_TLS_VERIFY=${GITEA_SKIP_TLS_VERIFY:-false}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=1 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
@@ -30,8 +30,21 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your-secret-key-change-this-in-production}
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||
# REVERSE PROXY: If you access Gitea Mirror through a reverse proxy (e.g. Nginx, Caddy, Traefik),
|
||||
# you MUST set these three variables to your external URL. Example:
|
||||
# BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
# If deployed under a path prefix (e.g. https://git.example.com/mirror), also set:
|
||||
# BASE_URL=/mirror
|
||||
# BETTER_AUTH_URL=https://git.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
- PUBLIC_BETTER_AUTH_URL=${PUBLIC_BETTER_AUTH_URL:-http://localhost:4321}
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS:-}
|
||||
# Optional: ENCRYPTION_SECRET will be auto-generated if not provided
|
||||
# - ENCRYPTION_SECRET=${ENCRYPTION_SECRET:-}
|
||||
# GitHub/Gitea Mirror Config
|
||||
@@ -47,6 +60,8 @@ services:
|
||||
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
||||
- ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false}
|
||||
- SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false}
|
||||
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||
- GITEA_URL=${GITEA_URL:-}
|
||||
- GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||
- GITEA_USERNAME=${GITEA_USERNAME:-}
|
||||
@@ -72,7 +87,11 @@ services:
|
||||
- HEADER_AUTH_AUTO_PROVISION=${HEADER_AUTH_AUTO_PROVISION:-false}
|
||||
- HEADER_AUTH_ALLOWED_DOMAINS=${HEADER_AUTH_ALLOWED_DOMAINS:-}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=3 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
@@ -120,161 +120,13 @@ fi
|
||||
# Dependencies are already installed during the Docker build process
|
||||
|
||||
# Initialize the database if it doesn't exist
|
||||
# Note: Drizzle migrations will be run automatically when the app starts (see src/lib/db/index.ts)
|
||||
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||
echo "Initializing database..."
|
||||
if [ -f "dist/scripts/init-db.js" ]; then
|
||||
bun dist/scripts/init-db.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js init
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts init
|
||||
else
|
||||
echo "Warning: Could not find database initialization scripts in dist/scripts."
|
||||
echo "Creating and initializing database manually..."
|
||||
|
||||
# Create the database file
|
||||
touch /app/data/gitea-mirror.db
|
||||
|
||||
# Initialize the database with required tables
|
||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
destination_org TEXT,
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||
forked_from TEXT,
|
||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
||||
language TEXT,
|
||||
description TEXT,
|
||||
default_branch TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
-- Uniqueness of (user_id, full_name) for repositories is enforced via drizzle migrations
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||
is_included INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
repository_name TEXT,
|
||||
organization_id TEXT,
|
||||
organization_name TEXT,
|
||||
details TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- New fields for job resilience
|
||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||
batch_id TEXT,
|
||||
total_items INTEGER,
|
||||
completed_items INTEGER DEFAULT 0,
|
||||
item_ids TEXT, -- JSON array as text
|
||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array as text
|
||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
last_checkpoint TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||
EOF
|
||||
echo "Database initialized with required tables."
|
||||
fi
|
||||
echo "Database not found. It will be created and initialized via Drizzle migrations on first app startup..."
|
||||
# Create empty database file so migrations can run
|
||||
touch /app/data/gitea-mirror.db
|
||||
else
|
||||
echo "Database already exists, checking for issues..."
|
||||
if [ -f "dist/scripts/fix-db-issues.js" ]; then
|
||||
bun dist/scripts/fix-db-issues.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js fix
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts fix
|
||||
fi
|
||||
|
||||
echo "Database exists, checking integrity..."
|
||||
echo "Database already exists, Drizzle will check for pending migrations on startup..."
|
||||
fi
|
||||
|
||||
# Extract version from package.json and set as environment variable
|
||||
@@ -287,16 +139,29 @@ fi
|
||||
|
||||
# Initialize configuration from environment variables if provided
|
||||
echo "Checking for environment configuration..."
|
||||
if [ -f "dist/scripts/startup-env-config.js" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun dist/scripts/startup-env-config.js
|
||||
ENV_CONFIG_EXIT_CODE=$?
|
||||
elif [ -f "scripts/startup-env-config.ts" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun scripts/startup-env-config.ts
|
||||
ENV_CONFIG_EXIT_CODE=$?
|
||||
|
||||
# Only run the env config script if relevant env vars are set
|
||||
# This avoids spawning a heavy Bun process on memory-constrained systems
|
||||
HAS_ENV_CONFIG=false
|
||||
if [ -n "$GITHUB_USERNAME" ] || [ -n "$GITHUB_TOKEN" ] || [ -n "$GITEA_URL" ] || [ -n "$GITEA_USERNAME" ] || [ -n "$GITEA_TOKEN" ]; then
|
||||
HAS_ENV_CONFIG=true
|
||||
fi
|
||||
|
||||
if [ "$HAS_ENV_CONFIG" = "true" ]; then
|
||||
if [ -f "dist/scripts/startup-env-config.js" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun dist/scripts/startup-env-config.js || ENV_CONFIG_EXIT_CODE=$?
|
||||
ENV_CONFIG_EXIT_CODE=${ENV_CONFIG_EXIT_CODE:-0}
|
||||
elif [ -f "scripts/startup-env-config.ts" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun scripts/startup-env-config.ts || ENV_CONFIG_EXIT_CODE=$?
|
||||
ENV_CONFIG_EXIT_CODE=${ENV_CONFIG_EXIT_CODE:-0}
|
||||
else
|
||||
echo "Environment configuration script not found. Skipping."
|
||||
ENV_CONFIG_EXIT_CODE=0
|
||||
fi
|
||||
else
|
||||
echo "Environment configuration script not found. Skipping."
|
||||
echo "No GitHub/Gitea environment variables found, skipping env config initialization."
|
||||
ENV_CONFIG_EXIT_CODE=0
|
||||
fi
|
||||
|
||||
@@ -309,17 +174,15 @@ fi
|
||||
|
||||
# Run startup recovery to handle any interrupted jobs
|
||||
echo "Running startup recovery..."
|
||||
RECOVERY_EXIT_CODE=0
|
||||
if [ -f "dist/scripts/startup-recovery.js" ]; then
|
||||
echo "Running startup recovery using compiled script..."
|
||||
bun dist/scripts/startup-recovery.js --timeout=30000
|
||||
RECOVERY_EXIT_CODE=$?
|
||||
bun dist/scripts/startup-recovery.js --timeout=30000 || RECOVERY_EXIT_CODE=$?
|
||||
elif [ -f "scripts/startup-recovery.ts" ]; then
|
||||
echo "Running startup recovery using TypeScript script..."
|
||||
bun scripts/startup-recovery.ts --timeout=30000
|
||||
RECOVERY_EXIT_CODE=$?
|
||||
bun scripts/startup-recovery.ts --timeout=30000 || RECOVERY_EXIT_CODE=$?
|
||||
else
|
||||
echo "Warning: Startup recovery script not found. Skipping recovery."
|
||||
RECOVERY_EXIT_CODE=0
|
||||
fi
|
||||
|
||||
# Log recovery result
|
||||
@@ -333,17 +196,15 @@ fi
|
||||
|
||||
# Run repository status repair to fix any inconsistent mirroring states
|
||||
echo "Running repository status repair..."
|
||||
REPAIR_EXIT_CODE=0
|
||||
if [ -f "dist/scripts/repair-mirrored-repos.js" ]; then
|
||||
echo "Running repository repair using compiled script..."
|
||||
bun dist/scripts/repair-mirrored-repos.js --startup
|
||||
REPAIR_EXIT_CODE=$?
|
||||
bun dist/scripts/repair-mirrored-repos.js --startup || REPAIR_EXIT_CODE=$?
|
||||
elif [ -f "scripts/repair-mirrored-repos.ts" ]; then
|
||||
echo "Running repository repair using TypeScript script..."
|
||||
bun scripts/repair-mirrored-repos.ts --startup
|
||||
REPAIR_EXIT_CODE=$?
|
||||
bun scripts/repair-mirrored-repos.ts --startup || REPAIR_EXIT_CODE=$?
|
||||
else
|
||||
echo "Warning: Repository repair script not found. Skipping repair."
|
||||
REPAIR_EXIT_CODE=0
|
||||
fi
|
||||
|
||||
# Log repair result
|
||||
@@ -368,7 +229,13 @@ trap 'shutdown_handler' TERM INT HUP
|
||||
|
||||
# Start the application
|
||||
echo "Starting Gitea Mirror..."
|
||||
bun ./dist/server/entry.mjs &
|
||||
if [ -f "./scripts/runtime-server.ts" ]; then
|
||||
bun ./scripts/runtime-server.ts &
|
||||
elif [ -f "./dist/scripts/runtime-server.js" ]; then
|
||||
bun ./dist/scripts/runtime-server.js &
|
||||
else
|
||||
bun ./dist/server/entry.mjs &
|
||||
fi
|
||||
APP_PID=$!
|
||||
|
||||
# Wait for the application to finish
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# API access with API keys
|
||||
|
||||
Gitea Mirror's web UI talks to a set of JSON endpoints under `/api`. With an API key you can call the same endpoints from scripts, CI pipelines and workflow tools such as n8n, without a browser session.
|
||||
|
||||
## Create a key
|
||||
|
||||
1. Sign in and open **Settings → Authentication**.
|
||||
2. In the **API Keys** section click **Create key**, give it a name and pick an expiry (never, 30 days, 90 days or a year).
|
||||
3. Copy the key. It is shown once; afterwards only its first characters are visible in the list.
|
||||
|
||||
Keys start with `gm_`, belong to the user who created them and carry the same rights as that user's session. Revoke a key from the same list at any time. Keys are stored hashed. They are not rate limited, so a runaway script is stopped by revoking its key.
|
||||
|
||||
## Send the key
|
||||
|
||||
Put the key in the `x-api-key` header. Every endpoint that accepts a session cookie accepts the header instead.
|
||||
|
||||
```bash
|
||||
curl -sS "https://mirror.example.com/api/github/repositories" \
|
||||
-H "x-api-key: gm_..."
|
||||
```
|
||||
|
||||
A missing, malformed, expired or revoked key gets `401 Unauthorized`:
|
||||
|
||||
```json
|
||||
{ "success": false, "error": "Unauthorized" }
|
||||
```
|
||||
|
||||
If the app is served under a path prefix (`BASE_URL=/mirror`), prefix every URL with it: `/mirror/api/...`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
The calls below cover the automation cases from issue #314. Response bodies show the fields you are most likely to use; the app returns more.
|
||||
|
||||
### List tracked repositories
|
||||
|
||||
`GET /api/github/repositories`
|
||||
|
||||
Despite the name it lists every repository the account tracks, whatever the configured source. Requires a saved configuration, otherwise `404`.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Repositories fetched successfully",
|
||||
"repositories": [
|
||||
{
|
||||
"id": "9b2f...",
|
||||
"name": "hello-world",
|
||||
"fullName": "octocat/hello-world",
|
||||
"owner": "octocat",
|
||||
"organization": null,
|
||||
"status": "mirrored",
|
||||
"mirroredLocation": "github-mirrors/hello-world",
|
||||
"lastMirrored": "2026-09-02T08:00:00.000Z",
|
||||
"errorMessage": null,
|
||||
"destinationOrg": null,
|
||||
"sourceProvider": "github",
|
||||
"isPrivate": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`status` is one of `imported`, `mirroring`, `mirrored`, `failed`, `syncing`, `synced`, `skipped`, `deleted`, `archived`.
|
||||
|
||||
### Add a repository
|
||||
|
||||
`POST /api/sync/repository`
|
||||
|
||||
```json
|
||||
{ "owner": "octocat", "repo": "hello-world" }
|
||||
```
|
||||
|
||||
Optional fields: `destinationOrg` to override the Gitea organization for this repository, `force: true` to refresh the metadata of a repository that is already tracked.
|
||||
|
||||
The repository is looked up on the configured source and added with status `imported`. It is not mirrored until you start a mirror (next call) or the scheduler picks it up.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `200` | Added. `repository.id` is what the mirror call needs. |
|
||||
| `400` | `owner` or `repo` missing. |
|
||||
| `404` | No configuration for this account, or the repository does not exist on the source. |
|
||||
| `409` | Already tracked. Send `force: true` to refresh it instead. |
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Repository added successfully",
|
||||
"repository": { "id": "9b2f...", "fullName": "octocat/hello-world", "status": "imported" }
|
||||
}
|
||||
```
|
||||
|
||||
### Start a mirror
|
||||
|
||||
`POST /api/job/mirror-repo`
|
||||
|
||||
```json
|
||||
{ "repositoryIds": ["9b2f..."] }
|
||||
```
|
||||
|
||||
Starts mirroring in the background and returns straight away. Poll the list endpoint to watch `status` move from `mirroring` to `mirrored` or `failed`.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `200` | Job started. `repositories` lists what was queued. |
|
||||
| `400` | `repositoryIds` missing or empty, or no source token saved. |
|
||||
| `404` | None of the ids belong to this account. |
|
||||
|
||||
`POST /api/job/sync-repo` takes the same body and re-syncs repositories that are already mirrored.
|
||||
|
||||
### Organizations
|
||||
|
||||
`POST /api/sync/organization` adds an organization the same way:
|
||||
|
||||
```json
|
||||
{ "org": "my-org", "role": "member" }
|
||||
```
|
||||
|
||||
`role` is the account's role in that organization (`member`, `admin`, `owner` or `billing_manager`). `409` means it is already tracked; `force: true` refreshes it. Then `POST /api/job/mirror-org` with `{ "organizationIds": ["..."] }` mirrors its repositories.
|
||||
|
||||
When several sources are connected, an optional `sourceId` picks which one the organization imports and mirrors from (the first connected source is the default):
|
||||
|
||||
```json
|
||||
{ "org": "my-org", "role": "member", "sourceId": "…" }
|
||||
```
|
||||
|
||||
`PATCH /api/organizations/:id` with `{ "sourceId": "…" }` re-pins an existing organization to another source, and `{ "sourceId": null }` clears the pin so the organization follows every source's repositories of the same name again. A pin whose source was removed falls back the same way.
|
||||
|
||||
### Reconcile the destination
|
||||
|
||||
`POST /api/cleanup/reconcile`
|
||||
|
||||
Compares what the destination (Gitea or Forgejo) holds with the repositories this account tracks, and reports four groups: mirrors on the destination that the database does not know about, rows whose mirror was moved to another owner, rows marked mirrored whose repository is gone, and a healthy count. Only mirrors whose source URL points at the configured source count as this app's. Native repositories, mirrors of other hosts and second copies of a tracked mirror are listed under `notManaged` and never touched. Useful after a lost or restored database (issue #284) or after transferring mirrors between owners in Gitea (issue #400).
|
||||
|
||||
```json
|
||||
{ "dryRun": true, "adoptUntracked": false, "relocateMoved": false, "resetMissing": false }
|
||||
```
|
||||
|
||||
The body is optional and every field defaults to the value above, so an empty `POST` is a dry run. With `dryRun: false`:
|
||||
|
||||
- `adoptUntracked: true` creates a row for each untracked mirror from its source URL, so scheduled sync and the cleanup service include it from then on. The row keeps the mirror where it is, even when the strategy would put it elsewhere.
|
||||
- `relocateMoved: true` points each moved row at the owner that holds its mirror now, so sync follows the transfer instead of failing. A row that failed because its mirror was not at the recorded place goes back to `mirrored`. Nothing changes on the destination.
|
||||
- `resetMissing: true` sets each missing row back to `imported` so the next mirror run recreates the mirror. Rows are never deleted.
|
||||
|
||||
Nothing is deleted or archived on either side by this call; the cleanup service keeps its own rules.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"dryRun": false,
|
||||
"report": {
|
||||
"untracked": [{ "location": "github-mirrors/hello-world", "originalUrl": "https://github.com/octocat/hello-world.git", "sourcePath": "octocat/hello-world", "isPrivate": false }],
|
||||
"missing": [{ "id": "9b2f...", "fullName": "octocat/gone", "location": "github-mirrors/gone" }],
|
||||
"moved": [{ "id": "c41a...", "fullName": "octocat/tools", "from": "github-mirrors/tools", "to": "archive/tools" }],
|
||||
"notManaged": [{ "location": "me/notes", "reason": "not a mirror" }],
|
||||
"unverified": [],
|
||||
"healthyCount": 40,
|
||||
"elsewhereCount": 0,
|
||||
"scannedOwners": ["e2e_admin", "github-mirrors", "archive"],
|
||||
"skippedOwners": ["starred"],
|
||||
"totalOnDestination": 43
|
||||
},
|
||||
"applied": { "adopted": 1, "reset": 1, "relocated": 1, "skipped": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
`applied` is `null` on a dry run. `scannedOwners` are the destination users and organizations that were listed; `skippedOwners` are ones the configuration or the database point at but the destination does not have. A row lands in `unverified` when the presence check itself failed, and is left alone.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `200` | Report computed, and applied when asked. |
|
||||
| `400` | A field has the wrong type, or the destination is not configured yet. |
|
||||
| `404` | No configuration for this account. |
|
||||
|
||||
### Move a mirror to another owner
|
||||
|
||||
`POST /api/repositories/{id}/move-mirror`
|
||||
|
||||
```json
|
||||
{ "destinationOrg": "archive" }
|
||||
```
|
||||
|
||||
Changes the repository's destination and transfers its existing mirror there on the destination (Gitea or Forgejo), for issue #400. `PATCH /api/repositories/{id}` with `destinationOrg` only changes the label: the mirror stays where it is and the new destination applies if the mirror is ever created again. Send `null` to remove the override and move the mirror back to where the strategy puts it.
|
||||
|
||||
The mirror is looked for at its recorded location first, then anywhere on the destination in case someone transferred it by hand. A target organization that does not exist is created, the way a mirror run creates one. When a mirror of the same source already sits under the target, the row is pointed at it and the old copy is left alone. The row is only written once the destination has accepted the transfer, so a refused move changes nothing.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"destinationOrg": "archive",
|
||||
"mirroredLocation": "archive/hello-world",
|
||||
"transfer": {
|
||||
"outcome": "moved",
|
||||
"from": "github-mirrors/hello-world",
|
||||
"to": "archive/hello-world",
|
||||
"message": "Moved octocat/hello-world from github-mirrors/hello-world to archive/hello-world."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| `transfer.outcome` | Meaning |
|
||||
| --- | --- |
|
||||
| `moved` | The destination moved the repository. `mirroredLocation` is the new place. |
|
||||
| `pending` | The token cannot create repositories under the new owner, so the destination waits for one of its owners to accept. The mirror keeps syncing where it is, and sync follows it once accepted. |
|
||||
| `recorded` | A mirror of the same source already sat under the target; the row now points at it. |
|
||||
| `not-mirrored` | The row has never been mirrored, so only the destination changed. |
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `200` | Done, see `transfer`. |
|
||||
| `400` | Bad owner name, destination not configured, or a GitHub or GitLab destination (`destination-unsupported`). |
|
||||
| `403` | The destination token may not transfer the repository (`forbidden`). |
|
||||
| `404` | The repository or the configuration does not belong to this account. |
|
||||
| `409` | Nothing changed. `code` says why: `not-on-destination`, `name-taken`, `transfer-pending`, or `destination-mismatch` for a row mirrored to another host. |
|
||||
| `502` | The destination refused for another reason (`destination-error`). |
|
||||
|
||||
`POST /api/organizations/{id}/move-mirrors` does the same for every mirrored repository of an organization:
|
||||
|
||||
```json
|
||||
{ "destinationOrg": "archive", "dryRun": true }
|
||||
```
|
||||
|
||||
`dryRun` defaults to `true` and answers with `plan.moves` (`id`, `fullName`, `from`, `to`) and `plan.skipped` (`fullName`, `reason`) without touching anything. Repositories with their own destination, starred repositories, rows that are not mirrored yet and rows in a running mirror or sync are skipped. With `dryRun: false` each planned move runs, the organization's `destinationOrg` is written, and the answer lists `moved`, `pending`, `recorded` and `failed` (`fullName`, `error`). A failed repository keeps syncing where it is.
|
||||
|
||||
## A complete example
|
||||
|
||||
Add a repository and mirror it in one go:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE="https://mirror.example.com"
|
||||
KEY="gm_..."
|
||||
|
||||
added=$(curl -sS -X POST "$BASE/api/sync/repository" \
|
||||
-H "x-api-key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"owner":"octocat","repo":"hello-world"}')
|
||||
|
||||
id=$(echo "$added" | jq -r '.repository.id')
|
||||
|
||||
curl -sS -X POST "$BASE/api/job/mirror-repo" \
|
||||
-H "x-api-key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"repositoryIds\":[\"$id\"]}"
|
||||
```
|
||||
|
||||
## Managing keys from the API
|
||||
|
||||
Manage keys through the settings UI or with a browser session. The key endpoints need a session cookie, not a key, so a leaked key cannot mint more keys, and like every cookie-authenticated `POST` to `/api/auth/*` they also need an `Origin` header (browsers send it on their own; a script must add `Origin: https://mirror.example.com`). The key itself is only for the app routes above. The endpoints live under `/api/auth/api-key/`:
|
||||
|
||||
| Call | Body | Result |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/auth/api-key/create` | `{ "name": "ci", "expiresIn": 2592000 }` (`expiresIn` in seconds, omit for no expiry) | The new key, including `key`, shown only here. |
|
||||
| `GET /api/auth/api-key/list` | | `{ "apiKeys": [...], "total": n }`. Each entry has `id`, `name`, `start`, `createdAt`, `lastRequest` and `expiresAt`, never the secret. |
|
||||
| `POST /api/auth/api-key/delete` | `{ "keyId": "..." }` | Revokes the key. |
|
||||
|
||||
## Notes
|
||||
|
||||
- A key can only act on the account that created it. Each account keeps its own source, destination and repositories.
|
||||
- Keys are not rate limited by the app. Source hosts still apply their own limits, and the app tracks GitHub's.
|
||||
- Deleting a user deletes their keys.
|
||||
@@ -1,175 +0,0 @@
|
||||
# Better Auth Migration Guide
|
||||
|
||||
This document describes the migration from the legacy authentication system to Better Auth.
|
||||
|
||||
## Overview
|
||||
|
||||
Gitea Mirror has been migrated to use Better Auth, a modern authentication library that provides:
|
||||
- Built-in support for email/password authentication
|
||||
- Session management with secure cookies
|
||||
- Database adapter with Drizzle ORM
|
||||
- Ready for OAuth2, OIDC, and SSO integrations
|
||||
- Type-safe authentication throughout the application
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. Database Schema
|
||||
|
||||
New tables added:
|
||||
- `sessions` - User session management
|
||||
- `accounts` - Authentication providers (credentials, OAuth, etc.)
|
||||
- `verification_tokens` - Email verification and password reset tokens
|
||||
|
||||
Modified tables:
|
||||
- `users` - Added `emailVerified` field
|
||||
|
||||
### 2. Authentication Flow
|
||||
|
||||
**Login:**
|
||||
- Users now log in with email instead of username
|
||||
- Endpoint: `/api/auth/sign-in/email`
|
||||
- Session cookies are automatically managed
|
||||
|
||||
**Registration:**
|
||||
- Users register with username, email, and password
|
||||
- Username is stored as an additional field
|
||||
- Endpoint: `/api/auth/sign-up/email`
|
||||
|
||||
### 3. API Routes
|
||||
|
||||
All auth routes are now handled by Better Auth's catch-all handler:
|
||||
- `/api/auth/[...all].ts` handles all authentication endpoints
|
||||
|
||||
Legacy routes have been backed up to `/src/pages/api/auth/legacy-backup/`
|
||||
|
||||
### 4. Session Management
|
||||
|
||||
Sessions are now managed by Better Auth:
|
||||
- Middleware automatically populates `context.locals.user` and `context.locals.session`
|
||||
- Use `useAuth()` hook in React components for client-side auth
|
||||
- Sessions expire after 30 days by default
|
||||
|
||||
## Future OIDC/SSO Configuration
|
||||
|
||||
The project is now ready for OIDC and SSO integrations. To enable:
|
||||
|
||||
### 1. Enable SSO Plugin
|
||||
|
||||
```typescript
|
||||
// src/lib/auth.ts
|
||||
import { sso } from "better-auth/plugins/sso";
|
||||
|
||||
export const auth = betterAuth({
|
||||
// ... existing config
|
||||
plugins: [
|
||||
sso({
|
||||
provisionUser: async (data) => {
|
||||
// Custom user provisioning logic
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Register OIDC Providers
|
||||
|
||||
```typescript
|
||||
// Example: Register an OIDC provider
|
||||
await authClient.sso.register({
|
||||
issuer: "https://idp.example.com",
|
||||
domain: "example.com",
|
||||
clientId: "your-client-id",
|
||||
clientSecret: "your-client-secret",
|
||||
providerId: "example-provider",
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Enable OIDC Provider Mode
|
||||
|
||||
To make Gitea Mirror act as an OIDC provider:
|
||||
|
||||
```typescript
|
||||
// src/lib/auth.ts
|
||||
import { oidcProvider } from "better-auth/plugins/oidc";
|
||||
|
||||
export const auth = betterAuth({
|
||||
// ... existing config
|
||||
plugins: [
|
||||
oidcProvider({
|
||||
loginPage: "/signin",
|
||||
consentPage: "/oauth/consent",
|
||||
metadata: {
|
||||
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Database Migration for SSO
|
||||
|
||||
When enabling SSO/OIDC, run migrations to add required tables:
|
||||
|
||||
```bash
|
||||
# Generate the schema
|
||||
bun drizzle-kit generate
|
||||
|
||||
# Apply the migration
|
||||
bun drizzle-kit migrate
|
||||
```
|
||||
|
||||
New tables that will be added:
|
||||
- `sso_providers` - SSO provider configurations
|
||||
- `oauth_applications` - OAuth2 client applications
|
||||
- `oauth_access_tokens` - OAuth2 access tokens
|
||||
- `oauth_consents` - User consent records
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required environment variables:
|
||||
|
||||
```env
|
||||
# Better Auth configuration
|
||||
BETTER_AUTH_SECRET=your-secret-key
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Legacy (kept for compatibility)
|
||||
JWT_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
## Migration Script
|
||||
|
||||
To migrate existing users to Better Auth:
|
||||
|
||||
```bash
|
||||
bun run migrate:better-auth
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Creates credential accounts for existing users
|
||||
2. Moves password hashes to the accounts table
|
||||
3. Preserves user creation dates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Login Issues
|
||||
- Ensure users log in with email, not username
|
||||
- Check that BETTER_AUTH_SECRET is set
|
||||
- Verify database migrations have been applied
|
||||
|
||||
### Session Issues
|
||||
- Clear browser cookies if experiencing session problems
|
||||
- Check middleware is properly configured
|
||||
- Ensure auth routes are accessible at `/api/auth/*`
|
||||
|
||||
### Development Tips
|
||||
- Use `bun db:studio` to inspect database tables
|
||||
- Check `/api/auth/session` to verify current session
|
||||
- Enable debug logging in Better Auth for troubleshooting
|
||||
|
||||
## Resources
|
||||
|
||||
- [Better Auth Documentation](https://better-auth.com)
|
||||
- [Better Auth Astro Integration](https://better-auth.com/docs/integrations/astro)
|
||||
- [Better Auth Plugins](https://better-auth.com/docs/plugins)
|
||||
@@ -1,206 +0,0 @@
|
||||
# Build Guide
|
||||
|
||||
This guide covers building the open-source version of Gitea Mirror.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Bun** >= 1.2.9 (primary runtime)
|
||||
- **Node.js** >= 20 (for compatibility)
|
||||
- **Git**
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Initialize database
|
||||
bun run init-db
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Start the application
|
||||
bun run start
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bun run build` | Production build |
|
||||
| `bun run dev` | Development server |
|
||||
| `bun run preview` | Preview production build |
|
||||
| `bun test` | Run tests |
|
||||
| `bun run cleanup-db` | Remove database files |
|
||||
|
||||
## Build Output
|
||||
|
||||
The build creates:
|
||||
- `dist/` - Production-ready server files
|
||||
- `.astro/` - Build cache (git-ignored)
|
||||
- `data/` - SQLite database location
|
||||
|
||||
## Development Build
|
||||
|
||||
For active development with hot reload:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Access the application at http://localhost:4321
|
||||
|
||||
## Production Build
|
||||
|
||||
```bash
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Test the build
|
||||
bun run preview
|
||||
|
||||
# Run in production
|
||||
bun run start
|
||||
```
|
||||
|
||||
## Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Build Docker image
|
||||
docker build -t gitea-mirror:latest .
|
||||
|
||||
# Run container
|
||||
docker run -p 3000:3000 gitea-mirror:latest
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_PATH=./data/gitea-mirror.db
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=your-secret-here
|
||||
|
||||
# GitHub Configuration
|
||||
GITHUB_TOKEN=ghp_...
|
||||
GITHUB_WEBHOOK_SECRET=...
|
||||
GITHUB_EXCLUDED_ORGS=org1,org2,org3 # Optional: Comma-separated list of organizations to exclude from sync
|
||||
|
||||
# Gitea Configuration
|
||||
GITEA_URL=https://your-gitea.com
|
||||
GITEA_TOKEN=...
|
||||
```
|
||||
|
||||
## Common Build Issues
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
```bash
|
||||
# Solution
|
||||
bun install
|
||||
```
|
||||
|
||||
### Database Not Initialized
|
||||
|
||||
```bash
|
||||
# Solution
|
||||
bun run init-db
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Change port
|
||||
PORT=3001 bun run dev
|
||||
```
|
||||
|
||||
### Build Cache Issues
|
||||
|
||||
```bash
|
||||
# Clear cache
|
||||
rm -rf .astro/ dist/
|
||||
bun run build
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
### Development Speed
|
||||
|
||||
- Use `bun run dev` for hot reload
|
||||
- Skip type checking during rapid development
|
||||
- Keep `.astro/` cache between builds
|
||||
|
||||
### Production Optimization
|
||||
|
||||
- Minification enabled automatically
|
||||
- Tree shaking removes unused code
|
||||
- Image optimization with Sharp
|
||||
|
||||
## Validation
|
||||
|
||||
After building, verify:
|
||||
|
||||
```bash
|
||||
# Check build output
|
||||
ls -la dist/
|
||||
|
||||
# Test server starts
|
||||
bun run start
|
||||
|
||||
# Check health endpoint
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
## CI/CD Build
|
||||
|
||||
Example GitHub Actions workflow:
|
||||
|
||||
```yaml
|
||||
name: Build and Test
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install
|
||||
- run: bun run build
|
||||
- run: bun test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
1. Check Bun version: `bun --version`
|
||||
2. Clear dependencies: `rm -rf node_modules && bun install`
|
||||
3. Check for syntax errors: `bunx tsc --noEmit`
|
||||
|
||||
### Runtime Errors
|
||||
|
||||
1. Check environment variables
|
||||
2. Verify database exists
|
||||
3. Check file permissions
|
||||
|
||||
## Performance
|
||||
|
||||
Expected build times:
|
||||
- Clean build: ~5-10 seconds
|
||||
- Incremental build: ~2-5 seconds
|
||||
- Development startup: ~1-2 seconds
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Configure with [Configuration Guide](./CONFIGURATION.md)
|
||||
- Deploy with [Deployment Guide](./DEPLOYMENT.md)
|
||||
- Set up authentication with [SSO Guide](./SSO-OIDC-SETUP.md)
|
||||
@@ -1 +0,0 @@
|
||||
../certs/README.md
|
||||
@@ -16,27 +16,22 @@ This guide covers the development workflow for the open-source Gitea Mirror.
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
2. **Install dependencies and seed the SQLite database**:
|
||||
```bash
|
||||
bun install
|
||||
bun run setup
|
||||
```
|
||||
|
||||
3. **Initialize database**:
|
||||
```bash
|
||||
bun run init-db
|
||||
```
|
||||
|
||||
4. **Configure environment**:
|
||||
3. **Configure environment (optional)**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
5. **Start development server**:
|
||||
4. **Start the development server**:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
@@ -45,29 +40,33 @@ bun run dev
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bun run dev` | Start development server with hot reload |
|
||||
| `bun run build` | Build for production |
|
||||
| `bun run preview` | Preview production build |
|
||||
| `bun test` | Run all tests |
|
||||
| `bun run dev` | Start the Bun + Astro dev server with hot reload |
|
||||
| `bun run build` | Build the production bundle |
|
||||
| `bun run preview` | Preview the production build locally |
|
||||
| `bun test` | Run the Bun test suite |
|
||||
| `bun test:watch` | Run tests in watch mode |
|
||||
| `bun run db:studio` | Open database GUI |
|
||||
| `bun run db:studio` | Launch Drizzle Kit Studio |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
gitea-mirror/
|
||||
├── src/
|
||||
│ ├── components/ # React components
|
||||
│ ├── pages/ # Astro pages & API routes
|
||||
│ ├── lib/ # Core logic
|
||||
│ │ ├── db/ # Database queries
|
||||
│ │ ├── utils/ # Helper functions
|
||||
│ │ └── modules/ # Module system
|
||||
│ ├── hooks/ # React hooks
|
||||
│ └── types/ # TypeScript types
|
||||
├── public/ # Static assets
|
||||
├── scripts/ # Utility scripts
|
||||
└── tests/ # Test files
|
||||
├── src/ # Application UI, API routes, and services
|
||||
│ ├── components/ # React components rendered inside Astro pages
|
||||
│ ├── pages/ # Astro pages and API routes (e.g., /api/*)
|
||||
│ ├── lib/ # Core logic: GitHub/Gitea clients, scheduler, recovery, db helpers
|
||||
│ │ ├── db/ # Drizzle adapter + schema
|
||||
│ │ ├── modules/ # Module wiring (jobs, integrations)
|
||||
│ │ └── utils/ # Shared utilities
|
||||
│ ├── hooks/ # React hooks
|
||||
│ ├── content/ # In-app documentation and templated content
|
||||
│ ├── layouts/ # Shared layout components
|
||||
│ ├── styles/ # Tailwind CSS entrypoints
|
||||
│ └── types/ # TypeScript types
|
||||
├── scripts/ # Bun scripts for DB management and maintenance
|
||||
├── www/ # Marketing site (Astro + MDX use cases)
|
||||
├── public/ # Static assets served by Vite/Astro
|
||||
└── tests/ # Dedicated integration/unit test helpers
|
||||
```
|
||||
|
||||
## Feature Development
|
||||
@@ -80,10 +79,10 @@ git checkout -b feature/my-feature
|
||||
```
|
||||
|
||||
2. **Plan your changes**:
|
||||
- UI components in `/src/components/`
|
||||
- API endpoints in `/src/pages/api/`
|
||||
- Database queries in `/src/lib/db/queries/`
|
||||
- Types in `/src/types/`
|
||||
- UI components live in `src/components/`
|
||||
- API endpoints live in `src/pages/api/`
|
||||
- Database logic is under `src/lib/db/` (schema + adapter)
|
||||
- Shared types are in `src/types/`
|
||||
|
||||
3. **Implement the feature**:
|
||||
|
||||
@@ -120,7 +119,7 @@ describe('My Feature', () => {
|
||||
|
||||
5. **Update documentation**:
|
||||
- Add JSDoc comments
|
||||
- Update README if needed
|
||||
- Update README/docs if needed
|
||||
- Document API changes
|
||||
|
||||
## Database Development
|
||||
@@ -311,26 +310,26 @@ 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 v2.23.0
|
||||
git push origin v2.23.0
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
5. **Create GitHub release**
|
||||
4. **Create GitHub release**
|
||||
|
||||
5. **CI version sync and chart publish (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.
|
||||
- After the release build succeeds, CI packages the Helm chart with the same version as chart version and `appVersion` and pushes it to `oci://ghcr.io/raylabshq/charts/gitea-mirror`. The chart's default image tag is `v<appVersion>`, so a default install runs that release.
|
||||
- CI then commits the same version to `package.json` and `helm/gitea-mirror/Chart.yaml` on `main` automatically.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -350,6 +349,6 @@ git push origin v2.23.0
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Check existing [issues](https://github.com/yourusername/gitea-mirror/issues)
|
||||
- Join [discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
||||
- Read the [FAQ](./FAQ.md)
|
||||
- Check existing [issues](https://github.com/RayLabsHQ/gitea-mirror/issues)
|
||||
- Join [discussions](https://github.com/RayLabsHQ/gitea-mirror/discussions)
|
||||
- Review project docs in [docs/README.md](./README.md)
|
||||
@@ -16,6 +16,7 @@ When environment variables are set:
|
||||
## Table of Contents
|
||||
|
||||
- [Core Configuration](#core-configuration)
|
||||
- [HTTPS / TLS](#https--tls)
|
||||
- [GitHub Configuration](#github-configuration)
|
||||
- [Gitea Configuration](#gitea-configuration)
|
||||
- [Mirror Options](#mirror-options)
|
||||
@@ -33,24 +34,86 @@ Essential application settings required for running Gitea Mirror.
|
||||
| `NODE_ENV` | Application environment | `production` | No |
|
||||
| `HOST` | Server host binding | `0.0.0.0` | No |
|
||||
| `PORT` | Server port | `4321` | No |
|
||||
| `BASE_URL` | Application base path. Use `/` for root deployments, or a prefix such as `/mirror` when serving behind a reverse-proxy path prefix. | `/` | No |
|
||||
| `DATABASE_URL` | Database connection URL | `sqlite://data/gitea-mirror.db` | No |
|
||||
| `BETTER_AUTH_SECRET` | Secret key for session signing (generate with: `openssl rand -base64 32`) | - | Yes |
|
||||
| `BETTER_AUTH_URL` | Primary base URL for authentication. This should be the main URL where your application is accessed. | `http://localhost:4321` | No |
|
||||
| `PUBLIC_BETTER_AUTH_URL` | Client-side auth URL for multi-origin access. Set this to your primary domain when you need to access the app from different origins (e.g., both IP and domain). The client will use this URL for all auth requests instead of the current browser origin. | - | No |
|
||||
| `BETTER_AUTH_URL` | Authentication origin (scheme + host only, e.g. `https://git.example.com`). Do **not** include a path — any path is automatically stripped, and `BASE_URL` is applied separately. | `http://localhost:4321` | No |
|
||||
| `PUBLIC_BETTER_AUTH_URL` | Client-side auth origin for multi-origin access (same rule: origin only, no path). Set this to your primary domain when you need to access the app from different origins (e.g., both IP and domain). The client will use this URL for all auth requests instead of the current browser origin. | - | No |
|
||||
| `BETTER_AUTH_TRUSTED_ORIGINS` | Trusted origins for authentication requests. Comma-separated list of URLs. Use this to specify additional access URLs (e.g., local IP + domain: `http://10.10.20.45:4321,https://gitea-mirror.mydomain.tld`), SSO providers, reverse proxies, etc. | - | No |
|
||||
| `BETTER_AUTH_LOG_LEVEL` | Better Auth logger verbosity. Set to `debug` to surface the full SSO/OIDC sign-in and callback trace when troubleshooting authentication. Accepted values: `debug`, `info`, `warn`, `error`. (Better Auth does **not** use the `DEBUG` env var.) | `warn` | No |
|
||||
| `ENCRYPTION_SECRET` | Optional encryption key for tokens (generate with: `openssl rand -base64 48`) | - | No |
|
||||
|
||||
## HTTPS / TLS
|
||||
|
||||
Gitea Mirror can terminate TLS directly via the underlying `@astrojs/node` adapter — useful when you don't want a separate reverse proxy. When both variables below are set, the server starts as a real HTTPS listener instead of HTTP.
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `SERVER_CERT_PATH` | Absolute path to the TLS certificate (PEM). Set together with `SERVER_KEY_PATH` to enable HTTPS. | - | No |
|
||||
| `SERVER_KEY_PATH` | Absolute path to the TLS private key (PEM). Set together with `SERVER_CERT_PATH` to enable HTTPS. | - | No |
|
||||
|
||||
**Example (systemd or `.env`):**
|
||||
|
||||
```bash
|
||||
SERVER_CERT_PATH=/etc/ssl/gitea-mirror/cert.pem
|
||||
SERVER_KEY_PATH=/etc/ssl/gitea-mirror/key.pem
|
||||
PORT=443
|
||||
BETTER_AUTH_URL=https://mirror.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://mirror.example.com
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The process must have read access to both files. When binding to `PORT=443`, grant the binary the `CAP_NET_BIND_SERVICE` capability (or run as a user allowed to bind privileged ports) rather than running as root.
|
||||
- If you already terminate TLS at a reverse proxy (nginx, Traefik, Caddy), leave these unset and let the proxy handle certificates.
|
||||
- Works in Docker too — mount your certs and set both paths to locations inside the container.
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Settings for connecting to and configuring GitHub repository sources.
|
||||
Settings for connecting to the source host. GitHub is the default and, together with a Gitea destination, the supported path; every other source or destination is beta. GitLab and Gitea/Forgejo (including Codeberg) are selected with `SOURCE_PROVIDER`; the username and token variables below then hold the account and token for that host. See [SOURCE_PROVIDERS.md](SOURCE_PROVIDERS.md) for what each source supports.
|
||||
|
||||
### Basic Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITHUB_USERNAME` | Your GitHub username | - | - |
|
||||
| `GITHUB_TOKEN` | GitHub personal access token (requires repo and admin:org scopes) | - | - |
|
||||
| `SOURCE_PROVIDER` | Which host repositories are pulled from. `gitea` also covers Forgejo and Codeberg. Anything other than `github` is beta. | `github` | `github`, `gitlab`, `gitea` |
|
||||
| `SOURCE_URL` | Base URL of the GitLab or Gitea/Forgejo instance. Ignored for GitHub, which uses `GH_API_URL`. | `https://gitlab.com` for GitLab, `https://codeberg.org` for Gitea | e.g. `https://gitlab.example.com` |
|
||||
| `GITHUB_USERNAME` | Your username on the source host | - | - |
|
||||
| `GITHUB_TOKEN` | Personal access token for the source host. GitHub needs the `repo` and `admin:org` scopes, GitLab `read_api` and `read_repository`, Gitea/Forgejo `read:repository`, `read:user` and `read:organization`. | - | - |
|
||||
| `GITHUB_TYPE` | GitHub account type | `personal` | `personal`, `organization` |
|
||||
| `GH_API_URL` | GitHub API base URL. Override this to point at GitHub Enterprise Server or Enterprise Cloud with data residency. | `https://api.github.com` | e.g. `https://ghe.example.com/api/v3`, `https://api.TENANT.ghe.com` |
|
||||
|
||||
### GitLab and Gitea/Forgejo sources
|
||||
|
||||
```bash
|
||||
# Mirror from a self hosted GitLab
|
||||
SOURCE_PROVIDER=gitlab
|
||||
SOURCE_URL=https://gitlab.example.com
|
||||
GITHUB_USERNAME=my-gitlab-user
|
||||
GITHUB_TOKEN=glpat-...
|
||||
|
||||
# Mirror from Codeberg (Forgejo)
|
||||
SOURCE_PROVIDER=gitea
|
||||
GITHUB_USERNAME=my-codeberg-user
|
||||
GITHUB_TOKEN=...
|
||||
```
|
||||
|
||||
Code, tags, wiki and LFS are mirrored from every source. Issues, pull requests, releases, labels, milestones and star lists need a GitHub source and are skipped for the others.
|
||||
|
||||
Once repositories have been imported the source is locked, and once anything has been mirrored the Gitea server URL is locked. A `SOURCE_PROVIDER`, `SOURCE_URL` or `GITEA_URL` value that disagrees with a locked host is ignored on boot with a warning; change the host on the Configuration page, where the change asks for confirmation.
|
||||
|
||||
### GitHub Enterprise (GHES / GHEC with data residency)
|
||||
|
||||
Set `GH_API_URL` to point Octokit at a non-`github.com` API endpoint:
|
||||
|
||||
```bash
|
||||
# GitHub Enterprise Server (self-hosted)
|
||||
GH_API_URL=https://ghe.example.com/api/v3
|
||||
|
||||
# GitHub Enterprise Cloud with data residency
|
||||
GH_API_URL=https://api.TENANT.ghe.com
|
||||
```
|
||||
|
||||
Standard GitHub Enterprise Cloud on `github.com` works with the default — no override needed. Use a personal access token issued by the target Enterprise instance for `GITHUB_TOKEN`.
|
||||
|
||||
### Repository Selection
|
||||
|
||||
@@ -59,9 +122,13 @@ Settings for connecting to and configuring GitHub repository sources.
|
||||
| `PRIVATE_REPOSITORIES` | Include private repositories | `false` | `true`, `false` |
|
||||
| `PUBLIC_REPOSITORIES` | Include public repositories | `true` | `true`, `false` |
|
||||
| `INCLUDE_ARCHIVED` | Include archived repositories | `false` | `true`, `false` |
|
||||
| `INCLUDE_COLLABORATOR_REPOS` | Include repositories where you are a collaborator (not just owned). Set to `false` to limit imports to repos you own. | `true` | `true`, `false` |
|
||||
| `SKIP_FORKS` | Skip forked repositories | `false` | `true`, `false` |
|
||||
| `MIRROR_STARRED` | Mirror starred repositories | `false` | `true`, `false` |
|
||||
| `MIRROR_STARRED_LISTS` | Optional comma-separated GitHub Star List names to mirror (only used when `MIRROR_STARRED=true`) | - | Comma-separated list names (empty = all starred repos) |
|
||||
| `STARRED_REPOS_ORG` | Organization name for starred repos | `starred` | Any string |
|
||||
| `STARRED_REPOS_MODE` | How starred repos are mirrored | `dedicated-org` | `dedicated-org`, `preserve-owner` |
|
||||
| `STARRED_DUPLICATE_STRATEGY` | Name collision strategy when two starred repos share a name from different owners (`suffix` = `repo-owner`, `prefix` = `owner-repo`) | `suffix` | `suffix`, `prefix`, `owner-org` |
|
||||
|
||||
### Organization Settings
|
||||
|
||||
@@ -69,7 +136,8 @@ Settings for connecting to and configuring GitHub repository sources.
|
||||
|----------|-------------|---------|---------|
|
||||
| `MIRROR_ORGANIZATIONS` | Mirror organization repositories | `false` | `true`, `false` |
|
||||
| `PRESERVE_ORG_STRUCTURE` | Preserve GitHub organization structure in Gitea | `false` | `true`, `false` |
|
||||
| `ONLY_MIRROR_ORGS` | Only mirror organization repos (skip personal) | `false` | `true`, `false` |
|
||||
| `ONLY_MIRROR_ORGS` | Only mirror organization repos (skip personal); sets `skipPersonalRepos: true` in GitHub config | `false` | `true`, `false` |
|
||||
| `INCLUDE_ORGANIZATIONS` | Opt-in allowlist: only mirror repos from these organizations (empty = all orgs you belong to). Sets `includeOrganizations` in GitHub config | - | Comma-separated org names |
|
||||
| `MIRROR_STRATEGY` | Repository organization strategy | `preserve` | `preserve`, `single-org`, `flat-user`, `mixed` |
|
||||
|
||||
### Advanced Settings
|
||||
@@ -77,20 +145,30 @@ 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
|
||||
|
||||
Settings for the destination Gitea instance.
|
||||
Settings for the destination. Gitea and Forgejo are pull mirrors; GitHub and GitLab are push targets served by the push engine (see [PUSH_TARGETS.md](PUSH_TARGETS.md)). The `GITEA_*` names apply to every destination kind.
|
||||
|
||||
### Connection Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_URL` | Gitea instance URL | - | Valid URL |
|
||||
| `DESTINATION_PROVIDER` | Which host mirrors are created on. Gitea and Forgejo share one API (labels and hints differ). GitHub and GitLab receive `git push` of branches and tags from this app. Anything other than `gitea` is beta. | `gitea` | `gitea`, `forgejo`, `github`, `gitlab` |
|
||||
| `GITEA_URL` | Destination instance URL. Optional for GitHub and GitLab, which default to github.com and gitlab.com; set it for GitHub Enterprise Server or a self hosted GitLab. | - | Valid URL |
|
||||
| `GITEA_EXTERNAL_URL` | Optional external/browser URL used for dashboard links. API and mirroring still use `GITEA_URL`. | - | Valid URL |
|
||||
| `GITEA_TOKEN` | Gitea access token | - | - |
|
||||
| `GITEA_USERNAME` | Gitea username | - | - |
|
||||
| `GITEA_ORGANIZATION` | Default organization for single-org strategy | `github-mirrors` | Any string |
|
||||
|
||||
### Push Targets (GitHub and GitLab destinations)
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `MIRROR_CLONE_DIR` | Where the push engine keeps its bare clones, one per repository. | `<data dir>/mirrors` | Absolute or relative path |
|
||||
| `PUSH_CONCURRENCY` | How many git fetch or push operations run at once across all repositories. | `2` | 1 to 32 |
|
||||
|
||||
### Repository Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
@@ -135,12 +213,17 @@ Control what content gets mirrored from GitHub to Gitea.
|
||||
|----------|-------------|---------|---------|
|
||||
| `MIRROR_RELEASES` | Mirror GitHub releases | `false` | `true`, `false` |
|
||||
| `RELEASE_LIMIT` | Maximum number of releases to mirror per repository | `10` | Number (1-100) |
|
||||
| `RELEASE_ASSET_LIMIT` | Upload release assets only for the newest N mirrored releases. Older releases still get their notes and tag. Unset means assets for every mirrored release, `0` means release notes only. Lowering it never deletes assets already uploaded | unset (all) | Number (0 or more) |
|
||||
| `MIRROR_WIKI` | Mirror wiki content | `false` | `true`, `false` |
|
||||
| `MIRROR_METADATA` | Master toggle for metadata mirroring | `false` | `true`, `false` |
|
||||
| `MIRROR_ISSUES` | Mirror issues (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_PULL_REQUESTS` | Mirror pull requests (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_LABELS` | Mirror labels (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_MILESTONES` | Mirror milestones (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_ISSUE_CONCURRENCY` | Number of issues processed in parallel. Set above `1` to speed up mirroring at the risk of out-of-order creation. | `3` | Integer ≥ 1 |
|
||||
| `MIRROR_PULL_REQUEST_CONCURRENCY` | Number of pull requests processed in parallel. Values above `1` may cause ordering differences. | `5` | Integer ≥ 1 |
|
||||
|
||||
> **Ordering vs Throughput:** Metadata now mirrors sequentially by default to preserve chronology. Increase the concurrency variables only if you can tolerate minor out-of-order entries.
|
||||
|
||||
## Automation Configuration
|
||||
|
||||
@@ -226,10 +309,10 @@ Configure automatic cleanup of old events and data.
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `CLEANUP_DELETE_FROM_GITEA` | Delete repositories from Gitea | `false` | `true`, `false` |
|
||||
| `CLEANUP_DELETE_FROM_GITEA` | Apply the orphaned-repo action on the Gitea side too. When `false` (default), cleanup only updates gitea-mirror's own database (orphans are marked archived or removed from the repo list) and the Gitea/Forgejo copies are left untouched | `false` | `true`, `false` |
|
||||
| `CLEANUP_DELETE_IF_NOT_IN_GITHUB` | Delete repos not found in GitHub (automatically enables cleanup) | `true` | `true`, `false` |
|
||||
| `CLEANUP_ORPHANED_REPO_ACTION` | Action for orphaned repositories. **Note**: `archive` is recommended to preserve backups | `archive` | `skip`, `archive`, `delete` |
|
||||
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `true` | `true`, `false` |
|
||||
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `false` | `true`, `false` |
|
||||
| `CLEANUP_PROTECTED_REPOS` | Comma-separated list of protected repository names | - | Comma-separated strings |
|
||||
|
||||
**🛡️ Safety Features (Backup Protection)**:
|
||||
@@ -242,10 +325,11 @@ Configure automatic cleanup of old events and data.
|
||||
- **Regular repositories**: Uses Gitea's native archive feature (PATCH `/repos/{owner}/{repo}` with `archived: true`)
|
||||
- Makes repository read-only while preserving all data
|
||||
- **Mirror repositories**: Uses rename strategy (Gitea API returns 422 for archiving mirrors)
|
||||
- Renamed with `[ARCHIVED]` prefix for clear identification
|
||||
- Renamed with `archived-` prefix for clear identification
|
||||
- Description updated with preservation notice and timestamp
|
||||
- Mirror interval set to 8760h (1 year) to minimize sync attempts
|
||||
- Repository remains fully accessible and cloneable
|
||||
- **Manual Sync Option**: Archived mirrors are still available on the Repositories page with automatic syncs disabled—use the `Manual Sync` action to refresh them on demand.
|
||||
|
||||
### Execution Settings
|
||||
|
||||
@@ -293,6 +377,7 @@ services:
|
||||
environment:
|
||||
# Core Configuration
|
||||
- NODE_ENV=production
|
||||
- BASE_URL=/
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- BETTER_AUTH_SECRET=your-secure-secret-here
|
||||
# Primary access URL:
|
||||
@@ -361,6 +446,24 @@ This setup allows you to:
|
||||
|
||||
**Important:** When accessing from different origins (IP vs domain), you'll need to log in separately on each origin as cookies cannot be shared across different origins for security reasons.
|
||||
|
||||
### Path Prefix Deployments
|
||||
|
||||
If you serve Gitea Mirror under a subpath such as `https://git.example.com/mirror`, set:
|
||||
|
||||
```bash
|
||||
# BASE_URL handles the path prefix — auth URLs stay as origin only
|
||||
BASE_URL=/mirror
|
||||
BETTER_AUTH_URL=https://git.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# → Auth endpoints resolve to: https://git.example.com/mirror/api/auth/*
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `BETTER_AUTH_URL` and `PUBLIC_BETTER_AUTH_URL` must be **origin only** (scheme + host). Do not include the base path — it is applied automatically from `BASE_URL`. Any path accidentally included is stripped.
|
||||
- `BETTER_AUTH_TRUSTED_ORIGINS` must also contain origins only (no path).
|
||||
- `BASE_URL` is applied at runtime, so prebuilt images can be reused with different path prefixes.
|
||||
|
||||
### Trusted Origins
|
||||
|
||||
The `BETTER_AUTH_TRUSTED_ORIGINS` variable serves multiple purposes:
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Extending Gitea Mirror
|
||||
|
||||
Gitea Mirror is designed with extensibility in mind through a module system.
|
||||
|
||||
## Module System
|
||||
|
||||
The application provides a module interface that allows extending functionality:
|
||||
|
||||
```typescript
|
||||
export interface Module {
|
||||
name: string;
|
||||
version: string;
|
||||
init(app: AppContext): Promise<void>;
|
||||
cleanup?(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Modules
|
||||
|
||||
You can create custom modules to add features:
|
||||
|
||||
```typescript
|
||||
// my-module.ts
|
||||
export class MyModule implements Module {
|
||||
name = 'my-module';
|
||||
version = '1.0.0';
|
||||
|
||||
async init(app: AppContext) {
|
||||
// Add your functionality
|
||||
app.addRoute('/api/my-endpoint', this.handler);
|
||||
}
|
||||
|
||||
async handler(context) {
|
||||
return new Response('Hello from my module!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module Context
|
||||
|
||||
Modules receive an `AppContext` with:
|
||||
- Database access
|
||||
- Event system
|
||||
- Route registration
|
||||
- Configuration
|
||||
|
||||
## Private Extensions
|
||||
|
||||
If you're developing private extensions:
|
||||
|
||||
1. Create a separate package/repository
|
||||
2. Implement the module interface
|
||||
3. Use Bun's linking feature for development:
|
||||
```bash
|
||||
# In your extension
|
||||
bun link
|
||||
|
||||
# In gitea-mirror
|
||||
bun link your-extension
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep modules focused on a single feature
|
||||
- Use TypeScript for type safety
|
||||
- Handle errors gracefully
|
||||
- Clean up resources in `cleanup()`
|
||||
- Document your module's API
|
||||
|
||||
## Community Modules
|
||||
|
||||
Share your modules with the community:
|
||||
- Create a GitHub repository
|
||||
- Tag it with `gitea-mirror-module`
|
||||
- Submit a PR to list it in our docs
|
||||
|
||||
For more details on the module system, see the source code in `/src/lib/modules/`.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Force-Push Protection
|
||||
|
||||
This document describes the smart force-push protection system introduced in gitea-mirror v3.11.0+.
|
||||
|
||||
## The Problem
|
||||
|
||||
GitHub repositories can be force-pushed at any time — rewriting history, deleting branches, or replacing commits entirely. When gitea-mirror syncs a force-pushed repo, the old history in Gitea is silently overwritten. Files, commits, and branches disappear with no way to recover them.
|
||||
|
||||
The original workaround (`backupBeforeSync: true`) created a full git bundle backup before **every** sync. This doesn't scale — a user with 100+ GiB of mirrors would need up to 2 TB of backup storage with default retention settings, even though force-pushes are rare.
|
||||
|
||||
## Solution: Smart Detection
|
||||
|
||||
Instead of backing up everything every time, the system detects force-pushes **before** they happen and only acts when needed.
|
||||
|
||||
### How Detection Works
|
||||
|
||||
Before each sync, the app compares branch SHAs between Gitea (the mirror) and GitHub (the source):
|
||||
|
||||
1. **Fetch branches from both sides** — lightweight API calls to get branch names and their latest commit SHAs
|
||||
2. **Compare each branch**:
|
||||
- SHAs match → nothing changed, no action needed
|
||||
- SHAs differ → check if the change is a normal push or a force-push
|
||||
3. **Ancestry check** — for branches with different SHAs, call GitHub's compare API to determine if the new SHA is a descendant of the old one:
|
||||
- **Fast-forward** (new SHA descends from old) → normal push, safe to sync
|
||||
- **Diverged** (histories split) → force-push detected
|
||||
- **404** (old SHA doesn't exist on GitHub anymore) → history was rewritten, force-push detected
|
||||
- **Branch deleted on GitHub** → flagged as destructive change
|
||||
|
||||
### What Happens on Detection
|
||||
|
||||
Depends on the configured strategy (see below):
|
||||
- **Backup strategies** (`always`, `on-force-push`): create a git bundle snapshot, then sync
|
||||
- **Block strategy** (`block-on-force-push`): halt the sync, mark the repo as `pending-approval`, wait for user action
|
||||
|
||||
### Fail-Open Design
|
||||
|
||||
If detection itself fails (GitHub rate limits, network errors, API outages), sync proceeds normally. Detection never blocks a sync due to its own failure. Individual branch check failures are skipped — one flaky branch doesn't affect the others.
|
||||
|
||||
## Backup Strategies
|
||||
|
||||
Configure via **Configuration → Connections → Destructive Update Protection**.
|
||||
|
||||
| Strategy | What It Does | Storage Cost | Best For |
|
||||
|---|---|---|---|
|
||||
| **Disabled** | No detection, no backups | Zero | Repos you don't care about losing |
|
||||
| **Always Backup** | Snapshot before every sync (original behavior) | High | Small mirror sets, maximum safety |
|
||||
| **Smart** (default) | Detect force-pushes, backup only when found | Near-zero normally | Most users — efficient protection |
|
||||
| **Block & Approve** | Detect force-pushes, block sync until approved | Zero | Critical repos needing manual review |
|
||||
|
||||
### Strategy Details
|
||||
|
||||
#### Disabled
|
||||
|
||||
Syncs proceed without any detection or backup. If a force-push happens on GitHub, the mirror silently overwrites.
|
||||
|
||||
#### Always Backup
|
||||
|
||||
Creates a git bundle snapshot before every sync regardless of whether a force-push occurred. This is the legacy behavior (equivalent to the old `backupBeforeSync: true`). Safe but expensive for large mirror sets.
|
||||
|
||||
#### Smart (`on-force-push`) — Recommended
|
||||
|
||||
Runs the force-push detection before each sync. On normal days (no force-pushes), syncs proceed without any backup overhead. When a force-push is detected, a snapshot is created before the sync runs.
|
||||
|
||||
This gives you protection when it matters with near-zero cost when it doesn't.
|
||||
|
||||
#### Block & Approve (`block-on-force-push`)
|
||||
|
||||
Runs detection and, when a force-push is found, **blocks the sync entirely**. The repository is marked as `pending-approval` and excluded from future scheduled syncs until you take action:
|
||||
|
||||
- **Approve**: creates a backup first, then syncs (safe)
|
||||
- **Dismiss**: clears the flag and resumes normal syncing (no backup)
|
||||
|
||||
Use this for repos where you want manual control over destructive changes.
|
||||
|
||||
## Additional Settings
|
||||
|
||||
These appear when any non-disabled strategy is selected:
|
||||
|
||||
### Snapshot Retention Count
|
||||
|
||||
How many backup snapshots to keep per repository. Oldest snapshots are deleted when this limit is exceeded. Default: **5**.
|
||||
|
||||
### Snapshot Retention Days
|
||||
|
||||
Maximum age (in days) for backup snapshots. Bundles older than this are deleted during retention enforcement, though at least one bundle is always kept. Set to `0` to disable time-based retention. Default: **30**.
|
||||
|
||||
### Snapshot Directory
|
||||
|
||||
Where git bundle backups are stored. Default: **`data/repo-backups`**. Bundles are organized as `<directory>/<owner>/<repo>/<timestamp>.bundle`.
|
||||
|
||||
### Block Sync on Snapshot Failure
|
||||
|
||||
Available for **Always Backup** and **Smart** strategies. When enabled, if the snapshot creation fails (disk full, permissions error, etc.), the sync is also blocked. When disabled, sync continues even if the snapshot couldn't be created.
|
||||
|
||||
Recommended: **enabled** if you rely on backups for recovery.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The old `backupBeforeSync` boolean is still recognized:
|
||||
|
||||
| Old Setting | New Equivalent |
|
||||
|---|---|
|
||||
| `backupBeforeSync: true` | `backupStrategy: "on-force-push"` |
|
||||
| `backupBeforeSync: false` | `backupStrategy: "disabled"` |
|
||||
| Neither set | `backupStrategy: "on-force-push"` (new default) |
|
||||
|
||||
Existing configurations are automatically mapped. The old field is deprecated but will continue to work.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
No new environment variables are required. The backup strategy is configured through the web UI and stored in the database alongside other config.
|
||||
|
||||
## API
|
||||
|
||||
### Approve/Dismiss Blocked Repos
|
||||
|
||||
When using the `block-on-force-push` strategy, repos that are blocked can be managed via the API:
|
||||
|
||||
```bash
|
||||
# Approve sync (creates backup first, then syncs)
|
||||
curl -X POST http://localhost:4321/api/job/approve-sync \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: <session>" \
|
||||
-d '{"repositoryIds": ["<id>"], "action": "approve"}'
|
||||
|
||||
# Dismiss (clear the block, resume normal syncing)
|
||||
curl -X POST http://localhost:4321/api/job/approve-sync \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: <session>" \
|
||||
-d '{"repositoryIds": ["<id>"], "action": "dismiss"}'
|
||||
```
|
||||
|
||||
Blocked repos also show an **Approve** / **Dismiss** button in the repository table UI.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/lib/utils/force-push-detection.ts` | Core detection: fetch branches, compare SHAs, check ancestry |
|
||||
| `src/lib/repo-backup.ts` | Strategy resolver, backup decision logic, bundle creation |
|
||||
| `src/lib/gitea-enhanced.ts` | Sync flow integration (calls detection + backup before mirror-sync) |
|
||||
| `src/pages/api/job/approve-sync.ts` | Approve/dismiss API endpoint |
|
||||
| `src/components/config/GitHubConfigForm.tsx` | Strategy selector UI |
|
||||
| `src/components/repositories/RepositoryTable.tsx` | Pending-approval badge + action buttons |
|
||||
|
||||
### Detection Flow
|
||||
|
||||
```
|
||||
syncGiteaRepoEnhanced()
|
||||
│
|
||||
├─ Resolve backup strategy (config → backupStrategy → backupBeforeSync → default)
|
||||
│
|
||||
├─ If strategy needs detection ("on-force-push" or "block-on-force-push"):
|
||||
│ │
|
||||
│ ├─ fetchGiteaBranches() — GET /api/v1/repos/{owner}/{repo}/branches
|
||||
│ ├─ fetchGitHubBranches() — octokit.paginate(repos.listBranches)
|
||||
│ │
|
||||
│ └─ For each Gitea branch where SHA differs:
|
||||
│ └─ checkAncestry() — octokit.repos.compareCommits()
|
||||
│ ├─ "ahead" or "identical" → fast-forward (safe)
|
||||
│ ├─ "diverged" or "behind" → force-push detected
|
||||
│ └─ 404/422 → old SHA gone → force-push detected
|
||||
│
|
||||
├─ If "block-on-force-push" + detected:
|
||||
│ └─ Set repo status to "pending-approval", return early
|
||||
│
|
||||
├─ If backup needed (always, or on-force-push + detected):
|
||||
│ └─ Create git bundle snapshot
|
||||
│
|
||||
└─ Proceed to mirror-sync
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Repos stuck in "pending-approval"**: Use the Approve or Dismiss buttons in the repository table, or call the approve-sync API endpoint.
|
||||
|
||||
**Detection always skipped**: Check the activity log for skip reasons. Common causes: Gitea repo not yet mirrored (first sync), GitHub API rate limits, network errors. All are fail-open by design.
|
||||
|
||||
**Backups consuming too much space**: Lower the retention count, or switch from "Always Backup" to "Smart" which only creates backups on actual force-pushes.
|
||||
|
||||
**False positives**: The detection compares branch-by-branch. A rebase (which is a force-push) will correctly trigger detection. If you routinely rebase branches, consider using "Smart" instead of "Block & Approve" to avoid constant approval prompts.
|
||||
@@ -0,0 +1,486 @@
|
||||
# Nix Deployment Guide
|
||||
|
||||
This guide covers deploying Gitea Mirror using Nix flakes. The Nix deployment follows the same minimal configuration philosophy as `docker-compose.alt.yml` - secrets are auto-generated, and everything else can be configured via the web UI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Nix 2.4+ installed
|
||||
- For NixOS module: NixOS 23.05+
|
||||
|
||||
### Enable Flakes (Recommended)
|
||||
|
||||
To enable flakes permanently and avoid typing flags, add to `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`:
|
||||
```
|
||||
experimental-features = nix-command flakes
|
||||
```
|
||||
|
||||
**Note:** If you don't enable flakes globally, add `--extra-experimental-features 'nix-command flakes'` to all nix commands shown below.
|
||||
|
||||
## Quick Start (Zero Configuration!)
|
||||
|
||||
### Run Immediately - No Setup Required
|
||||
|
||||
```bash
|
||||
# Run directly from the flake (local)
|
||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
||||
|
||||
# Or from GitHub (once published)
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# If you have flakes enabled globally, simply:
|
||||
nix run .#gitea-mirror
|
||||
```
|
||||
|
||||
That's it! On first run:
|
||||
- Secrets (`BETTER_AUTH_SECRET` and `ENCRYPTION_SECRET`) are auto-generated
|
||||
- Database is automatically created and initialized
|
||||
- Startup recovery and repair scripts run automatically
|
||||
- Access the web UI at http://localhost:4321
|
||||
|
||||
Everything else (GitHub credentials, Gitea settings, mirror options) is configured through the web interface after signup.
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# Enter development shell with all dependencies
|
||||
nix develop --extra-experimental-features 'nix-command flakes'
|
||||
|
||||
# Or use direnv for automatic environment loading (handles flags automatically)
|
||||
echo "use flake" > .envrc
|
||||
direnv allow
|
||||
```
|
||||
|
||||
### Build and Install
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
nix build --extra-experimental-features 'nix-command flakes'
|
||||
|
||||
# Run the built package
|
||||
./result/bin/gitea-mirror
|
||||
|
||||
# Install to your profile
|
||||
nix profile install --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
||||
```
|
||||
|
||||
## What Happens on First Run?
|
||||
|
||||
Following the same pattern as the Docker deployment, the Nix package automatically:
|
||||
|
||||
1. **Creates data directory**: `~/.local/share/gitea-mirror` (or `$DATA_DIR`)
|
||||
2. **Generates secrets** (stored securely in data directory):
|
||||
- `BETTER_AUTH_SECRET` - Session authentication (32-char hex)
|
||||
- `ENCRYPTION_SECRET` - Token encryption (48-char base64)
|
||||
3. **Initializes database**: SQLite database with Drizzle migrations
|
||||
4. **Runs startup scripts**:
|
||||
- Environment configuration loader
|
||||
- Crash recovery for interrupted jobs
|
||||
- Repository status repair
|
||||
5. **Starts the application** with graceful shutdown handling
|
||||
|
||||
## NixOS Module - Minimal Deployment
|
||||
|
||||
### Simplest Possible Configuration
|
||||
|
||||
Add to your NixOS configuration (`/etc/nixos/configuration.nix`):
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, gitea-mirror, ... }: {
|
||||
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
gitea-mirror.nixosModules.default
|
||||
{
|
||||
# That's it! Just enable the service
|
||||
services.gitea-mirror.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Apply with:
|
||||
```bash
|
||||
sudo nixos-rebuild switch
|
||||
```
|
||||
|
||||
Access at http://localhost:4321, sign up (first user is admin), and configure everything via the web UI.
|
||||
|
||||
### Production Configuration
|
||||
|
||||
For production with custom domain and firewall:
|
||||
|
||||
```nix
|
||||
{
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
host = "0.0.0.0";
|
||||
port = 4321;
|
||||
betterAuthUrl = "https://mirror.example.com";
|
||||
betterAuthTrustedOrigins = "https://mirror.example.com";
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
# Optional: Use with nginx reverse proxy
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."mirror.example.com" = {
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:4321";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced: Manual Secret Management
|
||||
|
||||
If you prefer to manage secrets manually (e.g., with sops-nix or agenix):
|
||||
|
||||
1. Create a secrets file:
|
||||
```bash
|
||||
# /var/lib/gitea-mirror/secrets.env
|
||||
BETTER_AUTH_SECRET=your-32-character-minimum-secret-key-here
|
||||
ENCRYPTION_SECRET=your-encryption-secret-here
|
||||
```
|
||||
|
||||
2. Reference it in your configuration:
|
||||
```nix
|
||||
{
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
environmentFile = "/var/lib/gitea-mirror/secrets.env";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Full Configuration Options
|
||||
|
||||
```nix
|
||||
{
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
package = gitea-mirror.packages.x86_64-linux.default; # Override package
|
||||
dataDir = "/var/lib/gitea-mirror";
|
||||
user = "gitea-mirror";
|
||||
group = "gitea-mirror";
|
||||
host = "0.0.0.0";
|
||||
port = 4321;
|
||||
betterAuthUrl = "https://mirror.example.com";
|
||||
betterAuthTrustedOrigins = "https://mirror.example.com";
|
||||
|
||||
# Concurrency controls (match docker-compose.alt.yml)
|
||||
mirrorIssueConcurrency = 3; # Set to 1 for perfect chronological order
|
||||
mirrorPullRequestConcurrency = 5; # Set to 1 for perfect chronological order
|
||||
|
||||
environmentFile = null; # Optional secrets file
|
||||
openFirewall = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Service Management (NixOS)
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start gitea-mirror
|
||||
|
||||
# Stop the service
|
||||
sudo systemctl stop gitea-mirror
|
||||
|
||||
# Restart the service
|
||||
sudo systemctl restart gitea-mirror
|
||||
|
||||
# Check status
|
||||
sudo systemctl status gitea-mirror
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u gitea-mirror -f
|
||||
|
||||
# Health check
|
||||
curl http://localhost:4321/api/health
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All variables from `docker-compose.alt.yml` are supported:
|
||||
|
||||
```bash
|
||||
# === AUTO-GENERATED (Don't set unless you want specific values) ===
|
||||
BETTER_AUTH_SECRET # Auto-generated, stored in data dir
|
||||
ENCRYPTION_SECRET # Auto-generated, stored in data dir
|
||||
|
||||
# === CORE SETTINGS (Have good defaults) ===
|
||||
DATA_DIR="$HOME/.local/share/gitea-mirror"
|
||||
DATABASE_URL="file:$DATA_DIR/gitea-mirror.db"
|
||||
HOST="0.0.0.0"
|
||||
PORT="4321"
|
||||
NODE_ENV="production"
|
||||
|
||||
# === BETTER AUTH (Override for custom domains) ===
|
||||
BETTER_AUTH_URL="http://localhost:4321"
|
||||
BETTER_AUTH_TRUSTED_ORIGINS="http://localhost:4321"
|
||||
PUBLIC_BETTER_AUTH_URL="http://localhost:4321"
|
||||
|
||||
# === CONCURRENCY CONTROLS ===
|
||||
MIRROR_ISSUE_CONCURRENCY=3 # Default: 3 (set to 1 for perfect order)
|
||||
MIRROR_PULL_REQUEST_CONCURRENCY=5 # Default: 5 (set to 1 for perfect order)
|
||||
|
||||
# === CONFIGURE VIA WEB UI (Not needed at startup) ===
|
||||
# GitHub credentials, Gitea settings, mirror options, scheduling, etc.
|
||||
# All configured after signup through the web interface
|
||||
```
|
||||
|
||||
## Database Management
|
||||
|
||||
The Nix package includes a database management helper:
|
||||
|
||||
```bash
|
||||
# Initialize database (done automatically on first run)
|
||||
gitea-mirror-db init
|
||||
|
||||
# Check database health
|
||||
gitea-mirror-db check
|
||||
|
||||
# Fix database issues
|
||||
gitea-mirror-db fix
|
||||
|
||||
# Reset users
|
||||
gitea-mirror-db reset-users
|
||||
```
|
||||
|
||||
## Home Manager Integration
|
||||
|
||||
For single-user deployments:
|
||||
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
let
|
||||
gitea-mirror = (import (fetchTarball "https://github.com/RayLabsHQ/gitea-mirror/archive/main.tar.gz")).packages.${pkgs.system}.default;
|
||||
in {
|
||||
home.packages = [ gitea-mirror ];
|
||||
|
||||
# Optional: Run as user service
|
||||
systemd.user.services.gitea-mirror = {
|
||||
Unit = {
|
||||
Description = "Gitea Mirror Service";
|
||||
After = [ "network.target" ];
|
||||
};
|
||||
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${gitea-mirror}/bin/gitea-mirror";
|
||||
Restart = "always";
|
||||
Environment = [
|
||||
"DATA_DIR=%h/.local/share/gitea-mirror"
|
||||
"HOST=127.0.0.1"
|
||||
"PORT=4321"
|
||||
];
|
||||
};
|
||||
|
||||
Install = {
|
||||
WantedBy = [ "default.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Image from Nix (Optional)
|
||||
|
||||
You can also use Nix to create a Docker image:
|
||||
|
||||
```nix
|
||||
# Add to flake.nix packages section
|
||||
dockerImage = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "gitea-mirror";
|
||||
tag = "latest";
|
||||
contents = [ self.packages.${system}.default pkgs.cacert pkgs.openssl ];
|
||||
config = {
|
||||
Cmd = [ "${self.packages.${system}.default}/bin/gitea-mirror" ];
|
||||
ExposedPorts = { "4321/tcp" = {}; };
|
||||
Env = [
|
||||
"DATA_DIR=/data"
|
||||
"DATABASE_URL=file:/data/gitea-mirror.db"
|
||||
];
|
||||
Volumes = { "/data" = {}; };
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Build and load:
|
||||
```bash
|
||||
nix build --extra-experimental-features 'nix-command flakes' .#dockerImage
|
||||
docker load < result
|
||||
docker run -p 4321:4321 -v gitea-mirror-data:/data gitea-mirror:latest
|
||||
```
|
||||
|
||||
## Comparison: Docker vs Nix
|
||||
|
||||
Both deployment methods follow the same philosophy:
|
||||
|
||||
| Feature | Docker Compose | Nix |
|
||||
|---------|---------------|-----|
|
||||
| **Configuration** | Minimal (only BETTER_AUTH_SECRET) | Zero config (auto-generated) |
|
||||
| **Secret Generation** | Auto-generated & persisted | Auto-generated & persisted |
|
||||
| **Database Init** | Automatic on first run | Automatic on first run |
|
||||
| **Startup Scripts** | Runs recovery/repair/env-config | Runs recovery/repair/env-config |
|
||||
| **Graceful Shutdown** | Signal handling in entrypoint | Signal handling in wrapper |
|
||||
| **Health Check** | Docker healthcheck | systemd timer (optional) |
|
||||
| **Updates** | `docker pull` | `nix flake update && nixos-rebuild` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Auto-Generated Secrets
|
||||
```bash
|
||||
# For standalone
|
||||
cat ~/.local/share/gitea-mirror/.better_auth_secret
|
||||
cat ~/.local/share/gitea-mirror/.encryption_secret
|
||||
|
||||
# For NixOS service
|
||||
sudo cat /var/lib/gitea-mirror/.better_auth_secret
|
||||
sudo cat /var/lib/gitea-mirror/.encryption_secret
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
```bash
|
||||
# Check if database exists
|
||||
ls -la ~/.local/share/gitea-mirror/gitea-mirror.db
|
||||
|
||||
# Reinitialize (deletes all data!)
|
||||
rm ~/.local/share/gitea-mirror/gitea-mirror.db
|
||||
gitea-mirror-db init
|
||||
```
|
||||
|
||||
### Permission Issues (NixOS)
|
||||
```bash
|
||||
sudo chown -R gitea-mirror:gitea-mirror /var/lib/gitea-mirror
|
||||
sudo chmod 700 /var/lib/gitea-mirror
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
```bash
|
||||
# Change port
|
||||
export PORT=8080
|
||||
gitea-mirror
|
||||
|
||||
# Or in NixOS config
|
||||
services.gitea-mirror.port = 8080;
|
||||
```
|
||||
|
||||
### View Startup Logs
|
||||
```bash
|
||||
# Standalone (verbose output on console)
|
||||
gitea-mirror
|
||||
|
||||
# NixOS service
|
||||
sudo journalctl -u gitea-mirror -f --since "5 minutes ago"
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
### Standalone Installation
|
||||
```bash
|
||||
# Update flake lock
|
||||
nix flake update --extra-experimental-features 'nix-command flakes'
|
||||
|
||||
# Rebuild
|
||||
nix build --extra-experimental-features 'nix-command flakes'
|
||||
|
||||
# Or update profile
|
||||
nix profile upgrade --extra-experimental-features 'nix-command flakes' gitea-mirror
|
||||
```
|
||||
|
||||
### NixOS
|
||||
```bash
|
||||
# Update input
|
||||
sudo nix flake lock --update-input gitea-mirror --extra-experimental-features 'nix-command flakes'
|
||||
|
||||
# Rebuild system
|
||||
sudo nixos-rebuild switch --flake .#your-hostname
|
||||
```
|
||||
|
||||
## Migration from Docker
|
||||
|
||||
To migrate from Docker to Nix while keeping your data:
|
||||
|
||||
1. **Stop Docker container:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.alt.yml down
|
||||
```
|
||||
|
||||
2. **Copy data directory:**
|
||||
```bash
|
||||
# For standalone
|
||||
cp -r ./data ~/.local/share/gitea-mirror
|
||||
|
||||
# For NixOS
|
||||
sudo cp -r ./data /var/lib/gitea-mirror
|
||||
sudo chown -R gitea-mirror:gitea-mirror /var/lib/gitea-mirror
|
||||
```
|
||||
|
||||
3. **Copy secrets (if you want to keep them):**
|
||||
```bash
|
||||
# Extract from Docker volume
|
||||
docker run --rm -v gitea-mirror_data:/data alpine \
|
||||
cat /data/.better_auth_secret > better_auth_secret
|
||||
docker run --rm -v gitea-mirror_data:/data alpine \
|
||||
cat /data/.encryption_secret > encryption_secret
|
||||
|
||||
# Copy to new location
|
||||
cp better_auth_secret ~/.local/share/gitea-mirror/.better_auth_secret
|
||||
cp encryption_secret ~/.local/share/gitea-mirror/.encryption_secret
|
||||
chmod 600 ~/.local/share/gitea-mirror/.*_secret
|
||||
```
|
||||
|
||||
4. **Start Nix version:**
|
||||
```bash
|
||||
gitea-mirror
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Example GitHub Actions workflow (see `.github/workflows/nix-build.yml`):
|
||||
|
||||
```yaml
|
||||
name: Nix Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- run: nix flake check
|
||||
- run: nix build --print-build-logs
|
||||
```
|
||||
|
||||
This uses:
|
||||
- **Determinate Nix Installer** - Fast, reliable Nix installation with flakes enabled by default
|
||||
- **Magic Nix Cache** - Free caching using GitHub Actions cache (no account needed)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Nix Manual](https://nixos.org/manual/nix/stable/)
|
||||
- [NixOS Options Search](https://search.nixos.org/options)
|
||||
- [Nix Pills Tutorial](https://nixos.org/guides/nix-pills/)
|
||||
- [Project Documentation](../README.md)
|
||||
- [Docker Deployment](../docker-compose.alt.yml) - Equivalent minimal config
|
||||
@@ -0,0 +1,322 @@
|
||||
# Nix Package Distribution Guide
|
||||
|
||||
This guide explains how Gitea Mirror is distributed via Nix and how users can consume it.
|
||||
|
||||
## Distribution Methods
|
||||
|
||||
### Method 1: Direct GitHub Usage (Zero Infrastructure)
|
||||
|
||||
**No CI, releases, or setup needed!** Users can consume directly from GitHub:
|
||||
|
||||
```bash
|
||||
# Latest from main branch
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Pin to specific commit
|
||||
nix run github:RayLabsHQ/gitea-mirror/abc123def
|
||||
|
||||
# Pin to git tag
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
1. Nix fetches the repository from GitHub
|
||||
2. Nix reads `flake.nix` and `flake.lock`
|
||||
3. Nix builds the package locally on the user's machine
|
||||
4. Package is cached in `/nix/store` for reuse
|
||||
|
||||
**Pros:**
|
||||
- Zero infrastructure needed
|
||||
- Works immediately after pushing code
|
||||
- Users always get reproducible builds
|
||||
|
||||
**Cons:**
|
||||
- Users must build from source (slower first time)
|
||||
- Requires build dependencies (Bun, etc.)
|
||||
|
||||
---
|
||||
|
||||
### Method 2: CI Build Caching
|
||||
|
||||
The GitHub Actions workflow uses **Magic Nix Cache** (by Determinate Systems) to cache builds:
|
||||
|
||||
- **Zero configuration required** - no accounts or tokens needed
|
||||
- **Automatic** - CI workflow handles everything
|
||||
- **Uses GitHub Actions cache** - fast, reliable, free
|
||||
|
||||
#### How It Works:
|
||||
|
||||
1. GitHub Actions builds the package on each push/PR
|
||||
2. Build artifacts are cached in GitHub Actions cache
|
||||
3. Subsequent builds reuse cached dependencies (faster CI)
|
||||
|
||||
Note: This caches CI builds. Users still build locally, but the flake.lock ensures reproducibility.
|
||||
|
||||
---
|
||||
|
||||
### Method 3: nixpkgs Submission (Official Distribution)
|
||||
|
||||
Submit to the official Nix package repository for maximum visibility.
|
||||
|
||||
#### Process:
|
||||
|
||||
1. **Prepare package** (already done with `flake.nix`)
|
||||
2. **Test thoroughly**
|
||||
3. **Submit PR to nixpkgs:** https://github.com/NixOS/nixpkgs
|
||||
|
||||
#### User Experience:
|
||||
|
||||
```bash
|
||||
# After acceptance into nixpkgs
|
||||
nix run nixpkgs#gitea-mirror
|
||||
|
||||
# NixOS configuration
|
||||
environment.systemPackages = [ pkgs.gitea-mirror ];
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Maximum discoverability (official repo)
|
||||
- Trusted by Nix community
|
||||
- Included in NixOS search
|
||||
- Binary caching by cache.nixos.org
|
||||
|
||||
**Cons:**
|
||||
- Submission/review process
|
||||
- Must follow nixpkgs guidelines
|
||||
- Updates require PRs
|
||||
|
||||
---
|
||||
|
||||
## Current Distribution Strategy
|
||||
|
||||
### Phase 1: Direct GitHub (Immediate) ✅
|
||||
|
||||
Already working! Users can:
|
||||
|
||||
```bash
|
||||
nix run github:RayLabsHQ/gitea-mirror
|
||||
```
|
||||
|
||||
### Phase 2: CI Build Validation ✅
|
||||
|
||||
GitHub Actions workflow validates builds on every push/PR:
|
||||
|
||||
- Uses Magic Nix Cache for fast CI builds
|
||||
- Tests on both Linux and macOS
|
||||
- No setup required - works automatically
|
||||
|
||||
### Phase 3: Version Releases (Optional)
|
||||
|
||||
Tag releases for version pinning:
|
||||
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
|
||||
# Users can then pin:
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
```
|
||||
|
||||
### Phase 4: nixpkgs Submission (Long Term)
|
||||
|
||||
Once package is stable and well-tested, submit to nixpkgs.
|
||||
|
||||
---
|
||||
|
||||
## User Documentation
|
||||
|
||||
### For Users: How to Install
|
||||
|
||||
Add this to your `docs/NIX_DEPLOYMENT.md`:
|
||||
|
||||
#### Option 1: Direct Install (No Configuration)
|
||||
|
||||
```bash
|
||||
# Run immediately
|
||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
|
||||
# Install to profile
|
||||
nix profile install --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
||||
```
|
||||
|
||||
#### Option 2: Pin to Specific Version
|
||||
|
||||
```bash
|
||||
# Pin to git tag
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
|
||||
# Pin to commit
|
||||
nix run github:RayLabsHQ/gitea-mirror/abc123def
|
||||
|
||||
# Lock in flake.nix
|
||||
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror/vX.Y.Z";
|
||||
```
|
||||
|
||||
#### Option 3: NixOS Configuration
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
|
||||
# Or pin to version:
|
||||
# gitea-mirror.url = "github:RayLabsHQ/gitea-mirror/vX.Y.Z";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, gitea-mirror, ... }: {
|
||||
nixosConfigurations.your-host = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
gitea-mirror.nixosModules.default
|
||||
{
|
||||
services.gitea-mirror = {
|
||||
enable = true;
|
||||
betterAuthUrl = "https://mirror.example.com";
|
||||
openFirewall = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintaining the Distribution
|
||||
|
||||
### Releasing New Versions
|
||||
|
||||
```bash
|
||||
# 1. Update version in package.json
|
||||
vim package.json # Update version field
|
||||
|
||||
# 2. Update flake.nix version (line 17)
|
||||
vim flake.nix # Update version = "X.Y.Z";
|
||||
|
||||
# 3. Commit changes
|
||||
git add package.json flake.nix
|
||||
git commit -m "chore: bump version to vX.Y.Z"
|
||||
|
||||
# 4. Create git tag
|
||||
git tag vX.Y.Z
|
||||
git push origin main
|
||||
git push origin vX.Y.Z
|
||||
|
||||
# 5. GitHub Actions builds and caches automatically
|
||||
```
|
||||
|
||||
Users can then pin to the new version:
|
||||
```bash
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
```
|
||||
|
||||
### Updating Flake Lock
|
||||
|
||||
The `flake.lock` file pins all dependencies. Update it periodically:
|
||||
|
||||
```bash
|
||||
# Update all inputs
|
||||
nix flake update
|
||||
|
||||
# Update specific input
|
||||
nix flake lock --update-input nixpkgs
|
||||
|
||||
# Test after update
|
||||
nix build
|
||||
nix flake check
|
||||
|
||||
# Commit the updated lock file
|
||||
git add flake.lock
|
||||
git commit -m "chore: update flake dependencies"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Distribution Issues
|
||||
|
||||
### Users Report Build Failures
|
||||
|
||||
1. **Check GitHub Actions:** Ensure CI is passing
|
||||
2. **Test locally:** `nix flake check`
|
||||
3. **Check flake.lock:** May need update if dependencies changed
|
||||
|
||||
### CI Cache Not Working
|
||||
|
||||
1. **Check workflow logs:** Review GitHub Actions for errors
|
||||
2. **Clear cache:** GitHub Actions → Caches → Delete relevant cache
|
||||
3. **Verify flake.lock:** May need `nix flake update` if dependencies changed
|
||||
|
||||
### Version Pinning Not Working
|
||||
|
||||
```bash
|
||||
# Verify tag exists
|
||||
git tag -l
|
||||
|
||||
# Ensure tag is pushed
|
||||
git ls-remote --tags origin
|
||||
|
||||
# Test specific tag
|
||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Custom Binary Cache
|
||||
|
||||
If you prefer self-hosting instead of Cachix:
|
||||
|
||||
### Option 1: S3-Compatible Storage
|
||||
|
||||
```nix
|
||||
# Generate signing key
|
||||
nix-store --generate-binary-cache-key cache.example.com cache-priv-key.pem cache-pub-key.pem
|
||||
|
||||
# Push to S3
|
||||
nix copy --to s3://my-nix-cache?region=us-east-1 $(nix-build)
|
||||
```
|
||||
|
||||
Users configure:
|
||||
```nix
|
||||
substituters = https://my-bucket.s3.amazonaws.com/nix-cache
|
||||
trusted-public-keys = cache.example.com:BASE64_PUBLIC_KEY
|
||||
```
|
||||
|
||||
### Option 2: Self-Hosted Nix Store
|
||||
|
||||
Run `nix-serve` on your server:
|
||||
|
||||
```bash
|
||||
# On server
|
||||
nix-serve -p 8080
|
||||
|
||||
# Behind nginx/caddy
|
||||
proxy_pass http://localhost:8080;
|
||||
```
|
||||
|
||||
Users configure:
|
||||
```nix
|
||||
substituters = https://cache.example.com
|
||||
trusted-public-keys = YOUR_KEY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Distribution Methods
|
||||
|
||||
| Method | Setup Time | User Speed | Cost | Discoverability |
|
||||
|--------|-----------|------------|------|-----------------|
|
||||
| Direct GitHub | 0 min | Slow (build) | Free | Low |
|
||||
| nixpkgs | Hours/days | Fast (binary) | Free | High |
|
||||
| Self-hosted cache | 30+ min | Fast (binary) | Server cost | Low |
|
||||
|
||||
**Current approach:** Direct GitHub consumption with CI validation using Magic Nix Cache. Users build locally (reproducible via flake.lock). Consider **nixpkgs** submission for maximum reach once the package is mature.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Nix Flakes Documentation](https://nixos.wiki/wiki/Flakes)
|
||||
- [Magic Nix Cache](https://github.com/DeterminateSystems/magic-nix-cache-action)
|
||||
- [nixpkgs Contributing Guide](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md)
|
||||
- [Nix Binary Cache Setup](https://nixos.org/manual/nix/stable/package-management/binary-cache-substituter.html)
|
||||
@@ -0,0 +1,135 @@
|
||||
# Notifications
|
||||
|
||||
Gitea Mirror supports push notifications for mirror events. You can be alerted when jobs succeed, fail, or when new repositories are discovered.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
### 1. Ntfy.sh (Direct)
|
||||
|
||||
[Ntfy.sh](https://ntfy.sh) is a simple HTTP-based pub-sub notification service. You can use the public server at `https://ntfy.sh` or self-host your own instance.
|
||||
|
||||
**Setup (public server):**
|
||||
1. Go to **Configuration > Notifications**
|
||||
2. Enable notifications and select **Ntfy.sh** as the provider
|
||||
3. Set the **Topic** to a unique name (e.g., `my-gitea-mirror-abc123`)
|
||||
4. Leave the Server URL as `https://ntfy.sh`
|
||||
5. Subscribe to the same topic on your phone or desktop using the [ntfy app](https://ntfy.sh/docs/subscribe/phone/)
|
||||
|
||||
**Setup (self-hosted):**
|
||||
1. Deploy ntfy using Docker: `docker run -p 8080:80 binwiederhier/ntfy serve`
|
||||
2. Set the **Server URL** to your instance (e.g., `http://ntfy:8080`)
|
||||
3. If authentication is enabled, provide an **Access token**
|
||||
4. Set your **Topic** name
|
||||
|
||||
**Priority levels:**
|
||||
- `min` / `low` / `default` / `high` / `urgent`
|
||||
- Error notifications automatically use `high` priority regardless of the default setting
|
||||
|
||||
### 2. Apprise API (Aggregator)
|
||||
|
||||
[Apprise](https://github.com/caronc/apprise-api) is a notification aggregator that supports 100+ services (Slack, Discord, Telegram, Email, Pushover, and many more) through a single API.
|
||||
|
||||
**Setup:**
|
||||
1. Deploy the Apprise API server:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
apprise:
|
||||
image: caronc/apprise:latest
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- apprise-config:/config
|
||||
volumes:
|
||||
apprise-config:
|
||||
```
|
||||
2. Configure your notification services in Apprise (via its web UI at `http://localhost:8000` or API)
|
||||
3. Create a configuration token/key in Apprise
|
||||
4. In Gitea Mirror, go to **Configuration > Notifications**
|
||||
5. Enable notifications and select **Apprise API**
|
||||
6. Set the **Server URL** to your Apprise instance (e.g., `http://apprise:8000`)
|
||||
7. Enter the **Token/path** you created in step 3
|
||||
|
||||
**Tag filtering:**
|
||||
- Optionally set a **Tag** to only notify specific Apprise services
|
||||
- Leave empty to notify all configured services
|
||||
|
||||
### 3. Gotify (Direct)
|
||||
|
||||
[Gotify](https://gotify.net/) is a simple self-hosted push notification server.
|
||||
|
||||
**Setup:**
|
||||
1. In Gotify, create an application and copy its token
|
||||
2. In Gitea Mirror, go to **Configuration > Notifications**
|
||||
3. Enable notifications and select **Gotify**
|
||||
4. Set the **Server URL** to your Gotify instance (e.g., `https://gotify.example.com`)
|
||||
5. Paste the **Application token**
|
||||
6. Pick a **Default priority** (0 to 10; error notifications are always sent at priority 8)
|
||||
|
||||
The token is encrypted at rest like all other provider credentials.
|
||||
|
||||
### 4. Webhook (Generic)
|
||||
|
||||
Sends a plain JSON POST to any URL you control. Works with n8n, Home Assistant, or any service that accepts a generic webhook.
|
||||
|
||||
**Setup:**
|
||||
1. In Gitea Mirror, go to **Configuration > Notifications**
|
||||
2. Enable notifications and select **Webhook**
|
||||
3. Set the **Webhook URL** to your endpoint
|
||||
4. Optionally set a **Signing secret**
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"title": "Mirror Failed: my-repo",
|
||||
"message": "Repository my-repo failed to mirror",
|
||||
"type": "sync_error",
|
||||
"timestamp": "2026-08-03T02:33:29.707Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Signature verification:**
|
||||
When a signing secret is set, every request includes an `X-Webhook-Signature` header containing `sha256=<hex>`, an HMAC-SHA256 digest of the raw request body. Verify it on the receiving end:
|
||||
|
||||
```js
|
||||
const crypto = require("node:crypto");
|
||||
const expected = "sha256=" +
|
||||
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
|
||||
const valid = crypto.timingSafeEqual(
|
||||
Buffer.from(signatureHeader), Buffer.from(expected));
|
||||
```
|
||||
|
||||
The secret is encrypted at rest.
|
||||
|
||||
## Event Types
|
||||
|
||||
| Event | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| Sync errors | On | A mirror job failed |
|
||||
| Sync success | Off | A mirror job completed successfully |
|
||||
| New repo discovered | Off | Not yet implemented; the toggle is disabled in the UI |
|
||||
|
||||
## Testing
|
||||
|
||||
Use the **Send Test Notification** button on the Notifications settings page to verify your configuration. The test sends a sample success notification to your configured provider.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Notifications not arriving:**
|
||||
- Check that notifications are enabled in the settings
|
||||
- Verify the provider configuration (URL, topic/token)
|
||||
- Use the Test button to check connectivity
|
||||
- Check the server logs for `[NotificationService]` messages
|
||||
|
||||
**Ntfy authentication errors:**
|
||||
- Ensure your access token is correct
|
||||
- If self-hosting, verify the ntfy server allows the topic
|
||||
|
||||
**Apprise connection refused:**
|
||||
- Verify the Apprise API server is running and accessible from the Gitea Mirror container
|
||||
- If using Docker, ensure both containers are on the same network
|
||||
- Check the Apprise server logs for errors
|
||||
|
||||
**Tokens and security:**
|
||||
- Notification tokens (ntfy access tokens, Apprise tokens) are encrypted at rest using the same AES-256-GCM encryption as GitHub/Gitea tokens
|
||||
- Tokens are decrypted only when sending notifications or displaying in the settings UI
|
||||
@@ -0,0 +1,85 @@
|
||||
# Push targets: GitHub and GitLab as destinations
|
||||
|
||||
Gitea and Forgejo are pull mirrors: the app asks them to fetch from the source, and they keep the copy up to date themselves. GitHub has no pull mirror API and GitLab only offers one on paid tiers, so for those two the app runs a push engine instead. This page explains what that engine does, what it needs, and what it does not do.
|
||||
|
||||
Push targets are marked **beta** in the destination dropdown, like every source and destination other than GitHub to Gitea. They are tested end to end against github.com and gitlab.com, but have had less time in the field than the default path.
|
||||
|
||||
## How it works
|
||||
|
||||
For every repository whose destination is GitHub or GitLab:
|
||||
|
||||
1. **A bare clone is kept on disk** under `<data dir>/mirrors/<source host>/<owner>/<name>.git`, next to the SQLite database. The first run clones, later runs fetch with prune, so a branch or tag deleted on the source is dropped from the clone.
|
||||
2. **The target repository is created when missing.** GitHub: under the account named in the connection card, or under an organization the mirror strategy picks. GitLab: a project under the user, or in a group; a missing top level group is created, a nested group is not.
|
||||
3. **Branches and tags are pushed** with force and prune. The target always ends up matching the source, including rewritten history and deleted branches. Pull request refs, merge request refs and other host owned namespaces are not pushed; hosts refuse writes to them.
|
||||
4. **The default branch is set** on the target to match the source, best effort.
|
||||
5. **Statuses, mirror jobs and activity rows** are the same as for a Gitea mirror, so the dashboard, the scheduler, recovery and cleanup treat both the same way. The repository table shows the destination's icon and links to the target repository.
|
||||
|
||||
The scheduler runs the same fetch and push on every scheduled sync. Both steps are idempotent: an interrupted run just runs again.
|
||||
|
||||
## The target is overwritten
|
||||
|
||||
Every push uses force and prune. Anything committed directly on the target, a branch created there, a tag moved there, is replaced by what the source has on the next sync. Treat a push target as read only, the same way a Gitea pull mirror is.
|
||||
|
||||
## What is not mirrored
|
||||
|
||||
Push targets receive **branches and tags only**. The following are not available for them and show as disabled in the settings:
|
||||
|
||||
- issues, pull requests, labels, milestones
|
||||
- releases and release assets (tags do travel)
|
||||
- wiki
|
||||
- LFS objects
|
||||
- force push protection and the backup strategies
|
||||
|
||||
Several destinations per repository, and pulling anything back from the target, are out of scope. Mirroring is one way.
|
||||
|
||||
## Credentials
|
||||
|
||||
Tokens never appear in a URL and are never written to disk. Each git invocation gets an inline credential helper that reads the token from an environment variable set on that process only: the source token for the fetch, the target token for the push. The remote URL stored in the bare clone is the plain source URL.
|
||||
|
||||
Token scopes:
|
||||
|
||||
| Target | Scopes | Notes |
|
||||
|--------|--------|-------|
|
||||
| GitHub | `repo`, `workflow` | `workflow` is needed as soon as a repository contains GitHub Actions files, or the push is rejected. Add `delete_repo` only if cleanup should delete repositories on GitHub. |
|
||||
| GitLab | `api`, `write_repository` | Pushes use the `oauth2` user name with the token, as GitLab documents. |
|
||||
|
||||
GitHub organizations cannot be created through the API. Create the organization first and make the token's user a member with permission to create repositories.
|
||||
|
||||
## Disk usage and concurrency
|
||||
|
||||
A bare clone takes roughly the size of the repository's git objects, so the data directory grows by about the size of everything you mirror this way. Clones are removed when the repository row is deleted, and by cleanup when it deletes an orphaned repository.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|----------|---------|---------|
|
||||
| `MIRROR_CLONE_DIR` | `<data dir>/mirrors` | Where the bare clones live. |
|
||||
| `PUSH_CONCURRENCY` | `2` | How many git fetch or push operations run at the same time across all repositories. API calls are cheap, a clone or a mirror push is not. |
|
||||
|
||||
One git process per repository at a time is enforced with a lock file next to the clone. A lock older than an hour belongs to a run that died and is taken over; a half written clone (no `HEAD`) is removed and cloned again.
|
||||
|
||||
## Large first pushes
|
||||
|
||||
GitHub limits the size of a single push. When the single push of all refs is refused, the engine retries in batches of 50 refs, branches first, then tags, followed by a prune. If a batch is still refused, the mirror fails with the git error in the repository's status.
|
||||
|
||||
## Cleanup
|
||||
|
||||
When the source deletes a repository, the cleanup service applies the configured action to the target through its API: archive on GitHub (`archived: true`) or GitLab (`/archive`), or delete. Deleting on GitHub needs the `delete_repo` scope; without it the cleanup run fails with a message naming the scope and the repository stays. On gitlab.com and instances with delayed deletion, a deleted project is renamed and kept for the configured number of days before it disappears. The bare clone is removed with the row.
|
||||
|
||||
The **Reconcile with destination** action compares a Gitea or Forgejo server with the database; it is not available for push targets, which have no server side mirror state to reconcile.
|
||||
|
||||
## Switching destinations
|
||||
|
||||
Once anything has been mirrored the destination is locked and changing it asks for confirmation, as for Gitea. Rows remember which destination they were mirrored to; a row mirrored to one host is refused on another until it is removed and mirrored again, so a token is never sent to the wrong host.
|
||||
|
||||
## Environment variables
|
||||
|
||||
```env
|
||||
DESTINATION_PROVIDER=github # or gitlab
|
||||
GITEA_URL=https://github.com # optional for github.com and gitlab.com; set for GHES or self hosted GitLab
|
||||
GITEA_USERNAME=your-account
|
||||
GITEA_TOKEN=your-token
|
||||
GITEA_ORGANIZATION=your-org # optional, used by the single-org strategy
|
||||
MIRROR_CLONE_DIR=/data/mirrors # optional
|
||||
PUSH_CONCURRENCY=2 # optional
|
||||
```
|
||||
|
||||
The `GITEA_*` names are kept for every destination kind so an existing deployment only has to change `DESTINATION_PROVIDER`. See [ENVIRONMENT_VARIABLES.md](ENVIRONMENT_VARIABLES.md).
|
||||
@@ -1,118 +1,46 @@
|
||||
# Gitea Mirror Documentation
|
||||
|
||||
Welcome to the Gitea Mirror documentation. This guide covers everything you need to know about developing, building, and deploying the open-source version of Gitea Mirror.
|
||||
This folder contains engineering and operations references for the open-source Gitea Mirror project. Each guide focuses on the parts of the system that still require bespoke explanation beyond the in-app help and the main `README.md`.
|
||||
|
||||
## Documentation Overview
|
||||
## Available Guides
|
||||
|
||||
### Getting Started
|
||||
### Core workflow
|
||||
- **[DEVELOPMENT_WORKFLOW.md](./DEVELOPMENT_WORKFLOW.md)** – Set up a local environment, run scripts, and understand the repo layout (app + marketing site).
|
||||
- **[ENVIRONMENT_VARIABLES.md](./ENVIRONMENT_VARIABLES.md)** – Complete reference for every configuration flag supported by the app and Docker images.
|
||||
- **[NIX_DEPLOYMENT.md](./NIX_DEPLOYMENT.md)** – User-facing deployment guide for Nix and NixOS.
|
||||
- **[NIX_DISTRIBUTION.md](./NIX_DISTRIBUTION.md)** – Maintainer notes for packaging, releases, and distribution strategy.
|
||||
|
||||
- **[Development Workflow](./DEVELOPMENT_WORKFLOW.md)** - Set up your development environment and start contributing
|
||||
- **[Build Guide](./BUILD_GUIDE.md)** - Build Gitea Mirror from source
|
||||
- **[Configuration Guide](./CONFIGURATION.md)** - Configure all available options
|
||||
### Reliability & recovery
|
||||
- **[GRACEFUL_SHUTDOWN.md](./GRACEFUL_SHUTDOWN.md)** – How signal handling, shutdown coordination, and job persistence work in v3.
|
||||
- **[RECOVERY_IMPROVEMENTS.md](./RECOVERY_IMPROVEMENTS.md)** – Deep dive into the startup recovery workflow and supporting scripts.
|
||||
|
||||
### Deployment
|
||||
### Authentication
|
||||
- **[SSO-OIDC-SETUP.md](./SSO-OIDC-SETUP.md)** – Configure OIDC/SSO providers through the admin UI.
|
||||
- **[SSO_TESTING.md](./SSO_TESTING.md)** – Recipes for local and staging SSO testing (Google, Keycloak, mock providers).
|
||||
|
||||
- **[Deployment Guide](./DEPLOYMENT.md)** - Deploy to production environments
|
||||
- **[Docker Guide](./DOCKER.md)** - Container-based deployment
|
||||
- **[Reverse Proxy Setup](./REVERSE_PROXY.md)** - Configure with nginx/Caddy
|
||||
### API
|
||||
- **[API.md](./API.md)** – Create API keys and call the app from scripts, CI pipelines and workflow tools.
|
||||
|
||||
### Features
|
||||
### Sources and destinations
|
||||
- **[SOURCE_PROVIDERS.md](./SOURCE_PROVIDERS.md)** – GitHub, GitLab and Gitea/Forgejo as sources: tokens, what each can mirror, the source lock.
|
||||
- **[PUSH_TARGETS.md](./PUSH_TARGETS.md)** – GitHub and GitLab as destinations: how the push engine works, token scopes, disk usage, what is not mirrored.
|
||||
|
||||
- **[SSO/OIDC Setup](./SSO-OIDC-SETUP.md)** - Configure authentication providers
|
||||
- **[Sponsor Integration](./SPONSOR_INTEGRATION.md)** - GitHub Sponsors integration
|
||||
- **[Webhook Configuration](./WEBHOOKS.md)** - Set up GitHub webhooks
|
||||
The user-facing documentation lives on the website at <https://gitea-mirror.raylabs.io/docs/>, with its sources under `www/src/pages/docs/`. If you are looking for playbooks, see the MDX use cases under `www/src/pages/use-cases/`.
|
||||
|
||||
### Architecture
|
||||
## Quick start for local development
|
||||
|
||||
- **[Architecture Overview](./ARCHITECTURE.md)** - System design and components
|
||||
- **[API Documentation](./API.md)** - REST API endpoints
|
||||
- **[Database Schema](./DATABASE.md)** - SQLite structure
|
||||
|
||||
### Maintenance
|
||||
|
||||
- **[Migration Guide](../MIGRATION_GUIDE.md)** - Upgrade from previous versions
|
||||
- **[Better Auth Migration](./BETTER_AUTH_MIGRATION.md)** - Migrate authentication system
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions
|
||||
- **[Backup & Restore](./BACKUP.md)** - Data management
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Clone and install**:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
bun install
|
||||
bun run setup # installs deps and seeds the SQLite DB
|
||||
bun run dev # starts the Astro/Bun app on http://localhost:4321
|
||||
```
|
||||
|
||||
2. **Configure**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your GitHub and Gitea tokens
|
||||
```
|
||||
The first user you create locally becomes the administrator. All other configuration—GitHub owners, Gitea targets, scheduling, cleanup—is done through the **Configuration** screen in the UI.
|
||||
|
||||
3. **Initialize and run**:
|
||||
```bash
|
||||
bun run init-db
|
||||
bun run dev
|
||||
```
|
||||
## Contributing & support
|
||||
|
||||
4. **Access**: Open http://localhost:4321
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🔄 **Automatic Syncing** - Keep repositories synchronized
|
||||
- 🗂️ **Organization Support** - Mirror entire organizations
|
||||
- ⭐ **Starred Repos** - Mirror your starred repositories
|
||||
- 🔐 **Self-Hosted** - Full control over your data
|
||||
- 🚀 **Fast** - Built with Bun for optimal performance
|
||||
- 🔒 **Secure** - JWT authentication, encrypted tokens
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Runtime**: Bun
|
||||
- **Framework**: Astro with React
|
||||
- **Database**: SQLite with Drizzle ORM
|
||||
- **Styling**: Tailwind CSS v4
|
||||
- **Authentication**: Better Auth
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Bun >= 1.2.9
|
||||
- Node.js >= 20 (optional, for compatibility)
|
||||
- SQLite 3
|
||||
- 512MB RAM minimum
|
||||
- 1GB disk space
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../CONTRIBUTING.md) for details.
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
Please read our [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/yourusername/gitea-mirror/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
||||
- **Wiki**: [GitHub Wiki](https://github.com/yourusername/gitea-mirror/wiki)
|
||||
|
||||
## Security
|
||||
|
||||
For security issues, please see [SECURITY.md](../SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
Gitea Mirror is open source software licensed under the [MIT License](../LICENSE).
|
||||
|
||||
---
|
||||
|
||||
For detailed information on any topic, please refer to the specific documentation guides listed above.
|
||||
- 🎯 Contribution guide: [../CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||
- 🐞 Issues & feature requests: <https://github.com/RayLabsHQ/gitea-mirror/issues>
|
||||
- 💬 Discussions: <https://github.com/RayLabsHQ/gitea-mirror/discussions>
|
||||
- 🔐 Security policy & advisories: <https://github.com/RayLabsHQ/gitea-mirror/security>
|
||||
@@ -63,9 +63,11 @@ The middleware now serves as a fallback recovery mechanism:
|
||||
|
||||
#### Improvements:
|
||||
- **Proper Drizzle ORM syntax** for all database queries
|
||||
- **Enhanced interrupted job detection** with multiple criteria:
|
||||
- **Enhanced interrupted job detection** with multiple criteria (see `src/lib/interrupted-job-detection.ts`):
|
||||
- Jobs with no recent checkpoint (10+ minutes)
|
||||
- Jobs that never wrote a checkpoint and started 10+ minutes ago (a job that just started is live, not interrupted)
|
||||
- Jobs running too long (2+ hours)
|
||||
- **Heartbeat during processing**: in-progress jobs start with a checkpoint and refresh it every 2 minutes, so one long item cannot make a live job look interrupted
|
||||
- **Detailed logging** of found interrupted jobs
|
||||
- **Better error handling** for database operations
|
||||
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# Graceful Shutdown Process
|
||||
|
||||
This document details how the gitea-mirror application handles graceful shutdown during active mirroring operations, with specific focus on job interruption and recovery.
|
||||
|
||||
## Overview
|
||||
|
||||
The graceful shutdown system is designed for **fast, clean termination** without waiting for long-running jobs to complete. It prioritizes **quick shutdown times** (under 30 seconds) while **preserving all progress** for seamless recovery.
|
||||
|
||||
## Key Principle
|
||||
|
||||
**The application does NOT wait for jobs to finish before shutting down.** Instead, it saves the current state and resumes after restart.
|
||||
|
||||
## Shutdown Scenario Example
|
||||
|
||||
### Initial State
|
||||
- **Job**: Mirror 500 repositories
|
||||
- **Progress**: 200 repositories completed
|
||||
- **Remaining**: 300 repositories pending
|
||||
- **Action**: User initiates shutdown (SIGTERM, Ctrl+C, Docker stop)
|
||||
|
||||
### Shutdown Process (Under 30 seconds)
|
||||
|
||||
#### Step 1: Signal Detection (Immediate)
|
||||
```
|
||||
📡 Received SIGTERM signal
|
||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
||||
```
|
||||
|
||||
#### Step 2: Job State Saving (1-10 seconds)
|
||||
```
|
||||
📝 Step 1: Saving active job states...
|
||||
Saving state for job abc-123...
|
||||
✅ Saved state for job abc-123
|
||||
```
|
||||
|
||||
**What gets saved:**
|
||||
- `inProgress: false` - Mark job as not currently running
|
||||
- `completedItems: 200` - Number of repos successfully mirrored
|
||||
- `totalItems: 500` - Total repos in the job
|
||||
- `completedItemIds: [repo1, repo2, ..., repo200]` - List of completed repos
|
||||
- `itemIds: [repo1, repo2, ..., repo500]` - Full list of repos
|
||||
- `lastCheckpoint: 2025-05-24T17:30:00Z` - Exact shutdown time
|
||||
- `message: "Job interrupted by application shutdown - will resume on restart"`
|
||||
- `status: "imported"` - Keeps status as resumable (not "failed")
|
||||
|
||||
#### Step 3: Service Cleanup (1-5 seconds)
|
||||
```
|
||||
🔧 Step 2: Executing shutdown callbacks...
|
||||
🛑 Shutting down cleanup service...
|
||||
✅ Cleanup service stopped
|
||||
✅ Shutdown callback 1 completed
|
||||
```
|
||||
|
||||
#### Step 4: Clean Exit (Immediate)
|
||||
```
|
||||
💾 Step 3: Closing database connections...
|
||||
✅ Graceful shutdown completed successfully
|
||||
```
|
||||
|
||||
**Total shutdown time: ~15 seconds** (well under the 30-second limit)
|
||||
|
||||
## What Happens to the Remaining 300 Repos?
|
||||
|
||||
### During Shutdown
|
||||
- **NOT processed** - The remaining 300 repos are not mirrored
|
||||
- **NOT lost** - Their IDs are preserved in the job state
|
||||
- **NOT marked as failed** - Job status remains "imported" for recovery
|
||||
|
||||
### After Restart
|
||||
The recovery system automatically:
|
||||
|
||||
1. **Detects interrupted job** during startup
|
||||
2. **Calculates remaining work**: 500 - 200 = 300 repos
|
||||
3. **Extracts remaining repo IDs**: repos 201-500 from the original list
|
||||
4. **Resumes processing** from exactly where it left off
|
||||
5. **Continues until completion** of all 500 repos
|
||||
|
||||
## Timeout Configuration
|
||||
|
||||
### Shutdown Timeouts
|
||||
```typescript
|
||||
const SHUTDOWN_TIMEOUT = 30000; // 30 seconds max shutdown time
|
||||
const JOB_SAVE_TIMEOUT = 10000; // 10 seconds to save job state
|
||||
```
|
||||
|
||||
### Timeout Behavior
|
||||
- **Normal case**: Shutdown completes in 10-20 seconds
|
||||
- **Slow database**: Up to 30 seconds allowed
|
||||
- **Timeout exceeded**: Force exit with code 1
|
||||
- **Container kill**: Orchestrator should allow 45+ seconds grace period
|
||||
|
||||
## Job State Persistence
|
||||
|
||||
### Database Schema
|
||||
The `mirror_jobs` table stores complete job state:
|
||||
|
||||
```sql
|
||||
-- Job identification
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||
|
||||
-- Progress tracking
|
||||
total_items INTEGER,
|
||||
completed_items INTEGER DEFAULT 0,
|
||||
item_ids TEXT, -- JSON array of all repo IDs
|
||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array of completed repo IDs
|
||||
|
||||
-- State management
|
||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean: currently running
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
last_checkpoint TIMESTAMP, -- Last progress save
|
||||
|
||||
-- Status and messaging
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL
|
||||
```
|
||||
|
||||
### Recovery Query
|
||||
The recovery system finds interrupted jobs:
|
||||
|
||||
```sql
|
||||
SELECT * FROM mirror_jobs
|
||||
WHERE in_progress = 0
|
||||
AND status = 'imported'
|
||||
AND completed_at IS NULL
|
||||
AND total_items > completed_items;
|
||||
```
|
||||
|
||||
## Shutdown-Aware Processing
|
||||
|
||||
### Concurrency Check
|
||||
During job execution, each repo processing checks for shutdown:
|
||||
|
||||
```typescript
|
||||
// Before processing each repository
|
||||
if (isShuttingDown()) {
|
||||
throw new Error('Processing interrupted by application shutdown');
|
||||
}
|
||||
```
|
||||
|
||||
### Checkpoint Intervals
|
||||
Jobs save progress periodically (every 10 repos by default):
|
||||
|
||||
```typescript
|
||||
checkpointInterval: 10, // Save progress every 10 repositories
|
||||
```
|
||||
|
||||
This ensures minimal work loss even if shutdown occurs between checkpoints.
|
||||
|
||||
## Container Integration
|
||||
|
||||
### Docker Entrypoint
|
||||
The Docker entrypoint properly forwards signals:
|
||||
|
||||
```bash
|
||||
# Set up signal handlers
|
||||
trap 'shutdown_handler' TERM INT HUP
|
||||
|
||||
# Start application in background
|
||||
bun ./dist/server/entry.mjs &
|
||||
APP_PID=$!
|
||||
|
||||
# Wait for application to finish
|
||||
wait "$APP_PID"
|
||||
```
|
||||
|
||||
### Kubernetes Configuration
|
||||
Recommended pod configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 45 # Allow time for graceful shutdown
|
||||
containers:
|
||||
- name: gitea-mirror
|
||||
# ... other configuration
|
||||
```
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Shutdown Logs
|
||||
```
|
||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
||||
📝 Step 1: Saving active job states...
|
||||
Saving state for 1 active jobs...
|
||||
✅ Completed saving all active jobs
|
||||
🔧 Step 2: Executing shutdown callbacks...
|
||||
✅ Completed all shutdown callbacks
|
||||
💾 Step 3: Closing database connections...
|
||||
✅ Graceful shutdown completed successfully
|
||||
```
|
||||
|
||||
### Recovery Logs
|
||||
```
|
||||
⚠️ Jobs found that need recovery. Starting recovery process...
|
||||
Resuming job abc-123 with 300 remaining items...
|
||||
✅ Recovery completed successfully
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Operations
|
||||
1. **Monitor shutdown times** - Should complete under 30 seconds
|
||||
2. **Check recovery logs** - Verify jobs resume correctly after restart
|
||||
3. **Set appropriate grace periods** - Allow 45+ seconds in orchestrators
|
||||
4. **Plan maintenance windows** - Jobs will resume but may take time to complete
|
||||
|
||||
### For Development
|
||||
1. **Test shutdown scenarios** - Use `bun run test-shutdown`
|
||||
2. **Monitor job progress** - Check checkpoint frequency and timing
|
||||
3. **Verify recovery** - Ensure interrupted jobs resume correctly
|
||||
4. **Handle edge cases** - Test shutdown during different job phases
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Shutdown Takes Too Long
|
||||
- **Check**: Database performance during job state saving
|
||||
- **Solution**: Increase `SHUTDOWN_TIMEOUT` environment variable
|
||||
- **Monitor**: Job complexity and checkpoint frequency
|
||||
|
||||
### Jobs Don't Resume
|
||||
- **Check**: Recovery logs for errors during startup
|
||||
- **Verify**: Database contains interrupted jobs with correct status
|
||||
- **Test**: Run `bun run startup-recovery` manually
|
||||
|
||||
### Container Force-Killed
|
||||
- **Check**: Container orchestrator termination grace period
|
||||
- **Increase**: Grace period to 45+ seconds
|
||||
- **Monitor**: Application shutdown completion time
|
||||
|
||||
This design ensures **production-ready graceful shutdown** with **zero data loss** and **fast recovery times** suitable for modern containerized deployments.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Source Providers
|
||||
|
||||
GitHub to Gitea is the supported default. Every other source and every other destination is marked **beta** in the dropdowns: they are tested end to end against real hosts, but have had less time in the field. The destination card has a matching **Destination** dropdown with Gitea, Forgejo (beta, same API as Gitea), GitHub (beta) and GitLab (beta); `DESTINATION_PROVIDER` sets it from the environment, and [PUSH_TARGETS.md](PUSH_TARGETS.md) covers the GitHub and GitLab targets.
|
||||
|
||||
Gitea Mirror pulls repositories from as many source hosts as the user connects, github.com and a GitHub Enterprise instance included. The connection card on the Configuration page is a list of sources with add, edit and remove. GitHub is the default, and GitLab and Gitea/Forgejo are available from the **Source** dropdown when a source is added or edited.
|
||||
|
||||
| Source | Covers | Default instance URL |
|
||||
|--------|--------|----------------------|
|
||||
| GitHub | github.com, GitHub Enterprise (via `GH_API_URL`) | `https://github.com` |
|
||||
| GitLab (beta) | gitlab.com, self hosted GitLab | `https://gitlab.com` |
|
||||
| Gitea / Forgejo (beta) | Codeberg, self hosted Gitea or Forgejo | `https://codeberg.org` |
|
||||
|
||||
Picking GitLab or Gitea/Forgejo shows an **Instance URL** field. Leave it empty for the default instance, or enter the base URL of your own (for example `https://gitlab.example.com` or `http://gitea.local:3000/gitea`). The username and token fields hold the account and token for the selected host.
|
||||
|
||||
## Tokens
|
||||
|
||||
| Source | Where to create the token | Scopes |
|
||||
|--------|---------------------------|--------|
|
||||
| GitHub | Settings, Developer settings, Personal access tokens (classic) | `repo`, `admin:org` |
|
||||
| GitLab | Preferences, Access tokens | `read_api`, `read_repository` |
|
||||
| Gitea / Forgejo | Settings, Applications | `read:repository`, `read:user`, `read:organization` |
|
||||
|
||||
The **Test** button on the card checks the token against the selected host.
|
||||
|
||||
## What is mirrored
|
||||
|
||||
The mirror itself is done by Gitea's pull mirror, which treats every source as a plain git remote. The differences are in what the source API offers.
|
||||
|
||||
| | GitHub | GitLab | Gitea / Forgejo |
|
||||
|---|:---:|:---:|:---:|
|
||||
| Code, branches, tags | yes | yes | yes |
|
||||
| Wiki | yes | yes | yes |
|
||||
| LFS objects | yes | yes | yes |
|
||||
| Scheduled sync | yes | yes | yes |
|
||||
| Auto import of new repositories | yes | yes | yes |
|
||||
| Cleanup of repositories deleted upstream | yes | yes | yes |
|
||||
| Starred repositories | yes | yes | yes |
|
||||
| Private repositories | yes | yes | yes |
|
||||
| Entire organizations (GitLab: top level groups) | yes | yes | yes |
|
||||
| Add a single repository by URL | yes | yes | yes |
|
||||
| Issues, pull requests, labels, milestones | yes | no | no |
|
||||
| Releases with assets | yes | no | no |
|
||||
| Star lists | yes | no | no |
|
||||
| Force push detection | yes | no | no |
|
||||
|
||||
The GitHub only rows read the GitHub API. For other sources the corresponding switches are disabled on the Configuration page, and the mirror step skips them.
|
||||
|
||||
Entire organizations also mirror without a token on every source; the next section covers public mode.
|
||||
|
||||
## Public organizations (no source connection)
|
||||
|
||||
An organization or group can be mirrored without any source account. Gitea Mirror lists the public repositories of a GitHub organization, a GitLab group or a Gitea/Forgejo organization on github.com, gitlab.com, a self hosted GitLab instance or a Gitea/Forgejo instance, and mirrors code, branches, tags, wiki and LFS objects through an anonymous mirror clone.
|
||||
|
||||
On the Organizations page, **Add organization** has a public mode, which is the default when no source is connected: pick the provider, fill in the instance URL for a self hosted GitLab or Gitea/Forgejo host (empty means the default instance), and enter the organization name. Saving creates a public only source row for that instance automatically, so the sources card does not have to be visited first. The row appears in the sources card with a public only indicator and can be edited or removed like any other source. New repositories added to the organization upstream are discovered on the next scheduled run.
|
||||
|
||||
Public mode degrades in known ways:
|
||||
|
||||
- **GitHub metadata is best effort.** Anonymous requests run under GitHub's 60 requests per hour limit, so a large organization can end up with partial metadata. Releases, description and topics mirror anonymously with no account at all. Issues, pull requests, labels and milestones are also fetched anonymously once any source in the account carries a token; with no token anywhere they are skipped, and the mirror itself is unaffected either way.
|
||||
- **GitLab and Gitea/Forgejo sources mirror code only.** Metadata remains GitHub only, as in the table above, and issues and merge requests from those sources stay a Not yet item with or without a token.
|
||||
- **Personal auto discovery and starred repositories need a token.** Neither runs for a public only source.
|
||||
- **Cleanup does not run.** Repositories deleted upstream are not detected for public only sources.
|
||||
|
||||
Listing and metadata go through the source API and are rate limited by the host when anonymous:
|
||||
|
||||
| Source | Anonymous API limit |
|
||||
|--------|---------------------|
|
||||
| GitHub | 60 requests per hour per IP address |
|
||||
| GitLab.com | 500 requests per minute per IP address. GitLab has announced a phased reduction to 60 requests per hour for unauthenticated API traffic, planned for late 2026 |
|
||||
| Gitea / Forgejo | No built in API rate limit. Instances may throttle or close anonymous API access on their own |
|
||||
|
||||
The mirror itself is a plain git clone and does not consume API requests.
|
||||
|
||||
Adding a username and token to the public only row on the Configuration page upgrades it to a regular source: private repositories, personal auto discovery and starred repositories start working, and GitHub metadata moves from the anonymous limit to the authenticated one. The public only row also enables adding a public repository by URL on that host, which needs a source row but no token.
|
||||
|
||||
The API accepts the same fields. `POST /api/sync/organization` adds a public organization like this:
|
||||
|
||||
```json
|
||||
{ "org": "acme", "role": "member", "provider": "gitlab", "sourceUrl": "https://gitlab.example.com" }
|
||||
```
|
||||
|
||||
`provider` is `github`, `gitlab` or `gitea`. `sourceUrl` is optional and defaults to the provider's default instance from the table at the top.
|
||||
|
||||
## Behaviour to know about
|
||||
|
||||
- **Each source locks once repositories are imported from it, and the destination locks once anything is mirrored.** A locked source's **Source** dropdown and instance URL, and the Gitea server URL, then show a lock note and a **Change** button. Changing them is still possible, but only after confirming a dialog that spells out what happens to the existing repositories. Removing a source that has repositories needs an explicit confirmation too. Saves that try to switch a locked source without that confirmation are refused by the API, and an environment variable that disagrees with a locked source is ignored on boot with a warning.
|
||||
- **Sources run side by side.** Every connected source discovers, imports and mirrors independently, and each repository remembers which source it came from. Cleanup checks each source's own repositories, and mirroring a repository uses its own source's credentials. Repositories whose source was removed keep syncing on the Gitea side, because Gitea holds the stored clone credentials, but re-mirroring, metadata and cleanup skip them.
|
||||
- **GitLab groups are flattened.** A project under `group/subgroup/project` lands in the Gitea organization `group` (with the preserve strategy) and keeps its full path as the repository's full name. The "Limit to specific groups" filter matches the top level group.
|
||||
- **GitLab internal projects are treated as private** when the mirror is created, because they are not visible to anonymous users on the source either.
|
||||
- **Repository names come from the URL path**, not the display name, so a GitLab project called "My Widget" with path `my-widget` mirrors as `my-widget`.
|
||||
- **Without a token** only the configured user's public repositories and starred repositories are visible on GitLab and Gitea/Forgejo. Adding a public repository by URL works without a token on every source.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `SOURCE_PROVIDER` | `github` (default), `gitlab` or `gitea` |
|
||||
| `SOURCE_URL` | Instance base URL for GitLab and Gitea/Forgejo |
|
||||
| `GITHUB_USERNAME` | Username on the selected host |
|
||||
| `GITHUB_TOKEN` | Token for the selected host |
|
||||
|
||||
These variables initialize or update the first source connection, creating it when none exists, and never switch a source that is locked. A `GH_API_URL` set for a single GitHub Enterprise deployment keeps applying to sources that point at github.com, so adding a github.com source in such a deployment sends its token to the configured Enterprise API; connect the Enterprise host as its own source instead.
|
||||
|
||||
See [ENVIRONMENT_VARIABLES.md](ENVIRONMENT_VARIABLES.md) for the full list.
|
||||
|
||||
## Not yet
|
||||
|
||||
Hosts without a usable API, and issues, merge requests and releases from GitLab, Gitea or Forgejo sources. Discovery preferences are shared, so filters apply to all sources rather than per source, and the GitHub and GitLab rate limit buckets are shared per provider. GitHub and GitLab as destinations shipped in 3.31.0 through the push engine, see [PUSH_TARGETS.md](PUSH_TARGETS.md).
|
||||
@@ -1,91 +0,0 @@
|
||||
# GitHub Sponsors Integration
|
||||
|
||||
This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror.
|
||||
|
||||
## Components
|
||||
|
||||
### GitHubSponsors Card
|
||||
|
||||
A card component that displays in the sidebar or dashboard:
|
||||
|
||||
```tsx
|
||||
import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your layout or dashboard
|
||||
<GitHubSponsors />
|
||||
```
|
||||
|
||||
### SponsorButton
|
||||
|
||||
A smaller button for headers or navigation:
|
||||
|
||||
```tsx
|
||||
import { SponsorButton } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your header
|
||||
<SponsorButton />
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. Dashboard Sidebar
|
||||
|
||||
Add the sponsor card to the dashboard sidebar for visibility:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/DashboardLayout.tsx
|
||||
<aside>
|
||||
{/* Other sidebar content */}
|
||||
<GitHubSponsors />
|
||||
</aside>
|
||||
```
|
||||
|
||||
### 2. Header Navigation
|
||||
|
||||
Add the sponsor button to the main navigation:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/Header.tsx
|
||||
<nav>
|
||||
{/* Other nav items */}
|
||||
<SponsorButton />
|
||||
</nav>
|
||||
```
|
||||
|
||||
### 3. Settings Page
|
||||
|
||||
Add a support section in settings:
|
||||
|
||||
```tsx
|
||||
// src/components/settings/SupportSection.tsx
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support Development</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GitHubSponsors />
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Only appears in self-hosted mode**: The components automatically hide in hosted mode
|
||||
- **Non-intrusive**: Designed to be helpful without being annoying
|
||||
- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo
|
||||
|
||||
## Customization
|
||||
|
||||
You can customize the sponsor components by:
|
||||
|
||||
1. Updating the GitHub Sponsors URL
|
||||
2. Adding/removing donation platforms
|
||||
3. Changing the styling to match your theme
|
||||
4. Adjusting the placement based on user feedback
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Don't be pushy**: Show sponsor options tastefully
|
||||
2. **Provide value first**: Ensure the tool is useful before asking for support
|
||||
3. **Be transparent**: Explain how sponsorships help the project
|
||||
4. **Thank sponsors**: Acknowledge supporters in README or releases
|
||||
@@ -4,15 +4,16 @@ This guide explains how to configure Single Sign-On (SSO) and OpenID Connect (OI
|
||||
|
||||
## Overview
|
||||
|
||||
Gitea Mirror supports three authentication methods:
|
||||
Gitea Mirror supports four authentication methods:
|
||||
|
||||
1. **Email & Password** - Traditional authentication (always enabled)
|
||||
2. **SSO (Single Sign-On)** - Allow users to authenticate using external OIDC providers
|
||||
3. **OIDC Provider** - Allow other applications to authenticate users through Gitea Mirror
|
||||
3. **Header / Forward Authentication** - Trust pre-authenticated requests from a reverse proxy (Authentik, Authelia, oauth2-proxy, etc.)
|
||||
4. **OIDC Provider** - Allow other applications to authenticate users through Gitea Mirror
|
||||
|
||||
## Configuration
|
||||
|
||||
All SSO and OIDC settings are managed through the web UI in the Configuration page under the "Authentication" tab.
|
||||
SSO and OIDC Provider settings are managed through the web UI in the Configuration page under the "Authentication" tab. Header / Forward Authentication is configured via environment variables only — see [Setting up Header / Forward Authentication](#setting-up-header--forward-authentication) below.
|
||||
|
||||
## Setting up SSO (Single Sign-On)
|
||||
|
||||
@@ -27,7 +28,7 @@ SSO allows your users to sign in using external identity providers like Google,
|
||||
#### Required Fields
|
||||
|
||||
- **Issuer URL**: The OIDC issuer URL (e.g., `https://accounts.google.com`)
|
||||
- **Domain**: The email domain for this provider (e.g., `example.com`)
|
||||
- **Domain**: The email domain this provider serves (e.g., `example.com`). **This is load-bearing for account-linking trust — see [Account Linking](#account-linking) below.** Multi-domain IdPs can use a comma-separated list (`example.com,subsidiary.com`).
|
||||
- **Provider ID**: A unique identifier for this provider (e.g., `google-sso`)
|
||||
- **Client ID**: The OAuth client ID from your provider
|
||||
- **Client Secret**: The OAuth client secret from your provider
|
||||
@@ -56,6 +57,22 @@ https://your-domain.com/api/auth/sso/callback/{provider-id}
|
||||
|
||||
Replace `{provider-id}` with your chosen Provider ID.
|
||||
|
||||
### Account Linking
|
||||
|
||||
When a user signs in via SSO whose email matches an **existing** email/password account, Gitea Mirror will try to *link* the SSO identity to that account so the user lands on the same dashboard — instead of being bounced to the login page or creating a duplicate account.
|
||||
|
||||
The trust model is **domain-scoped**:
|
||||
|
||||
- Every SSO provider you register is automatically marked `domainVerified: true` for the **Domain** field you set on registration.
|
||||
- An SSO sign-in is auto-linked to an existing local account **only when** the user's email address actually belongs to that registered domain. Cross-domain emails are rejected even if the provider is registered.
|
||||
- Local email/password accounts in this app are never verified (no email-verification flow exists), so we deliberately turn off `requireLocalEmailVerified` on the linker — otherwise no one could ever link.
|
||||
|
||||
**What this means for you:**
|
||||
|
||||
- Set the **Domain** field to the email domain(s) your IdP actually issues identities for. Don't set it to `example.com` if your IdP issues `@elsewhere.com` emails — auto-linking will silently refuse them.
|
||||
- If your IdP allows users to self-register or claim arbitrary emails *within the registered domain*, an attacker on that IdP can absorb a local account by claiming the same email. This is a trust decision: by registering an IdP, you're vouching for its identity model for that domain.
|
||||
- Multi-domain IdPs (one Authentik serving `acme.com,acquired.io`) work fine — list every domain comma-separated in the **Domain** field.
|
||||
|
||||
### Example: Google SSO Setup
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
@@ -81,6 +98,98 @@ Replace `{provider-id}` with your chosen Provider ID.
|
||||
- Client Secret: [Your Okta Client Secret]
|
||||
- Click "Discover" to auto-fill endpoints
|
||||
|
||||
### Example: Authentik SSO Setup
|
||||
|
||||
Working Authentik deployments (see [#134](https://github.com/RayLabsHQ/gitea-mirror/issues/134)) follow these steps:
|
||||
|
||||
1. In Authentik, create a new **Application** and OIDC **Provider** (implicit flow works well for testing).
|
||||
2. Start creating an SSO provider inside Gitea Mirror so you can copy the redirect URL shown (`https://your-domain.com/api/auth/sso/callback/authentik` if you pick `authentik` as your Provider ID).
|
||||
3. Paste that redirect URL into the Authentik Provider configuration and finish creating the provider.
|
||||
4. Copy the Authentik issuer URL, client ID, and client secret.
|
||||
5. Back in Gitea Mirror:
|
||||
- Issuer URL: the exact value from Authentik (keep any trailing slash Authentik shows).
|
||||
- Provider ID: match the one you used in step 2.
|
||||
- Click **Discover** so Gitea Mirror stores the authorization, token, and JWKS endpoints (Authentik publishes them via discovery).
|
||||
- Domain: enter the email domain you expect to match (e.g. `example.com`).
|
||||
6. Save the provider and test the login flow.
|
||||
|
||||
Notes:
|
||||
- Make sure `BETTER_AUTH_URL` and (if you serve the UI from multiple origins) `BETTER_AUTH_TRUSTED_ORIGINS` point at the public URL users reach. A mismatch can surface as 500 errors after redirect.
|
||||
- **Internal / split-DNS IdPs**: since v3.21.0 the SSO sign-in refuses identity providers whose hostnames resolve to private addresses (SSRF hardening in the auth library) unless the IdP's origin is trusted. Providers you register through the UI are trusted automatically, but if you register a *new* provider whose hostname resolves to a LAN IP from inside the container (common with split-horizon DNS), add its origin to `BETTER_AUTH_TRUSTED_ORIGINS` first, e.g. `BETTER_AUTH_TRUSTED_ORIGINS=https://auth.example.com`.
|
||||
- Set the **Domain** field to the email domain your Authentik users actually have. Auto-linking to an existing local admin only happens for emails in that domain — see [Account Linking](#account-linking) for the trust model. (Authentik's default email scope mapping returns `email_verified: False` for OIDC clients, which is why account linking is gated on the domain match here rather than the IdP's verified-email claim.)
|
||||
- If you created an Authentik provider before v3.8.10 you should delete it and re-add it after upgrading; older versions saved incomplete endpoint data which leads to the `url.startsWith` error explained in the Troubleshooting section.
|
||||
|
||||
## Setting up Header / Forward Authentication
|
||||
|
||||
Header authentication trusts a reverse proxy (Authentik, Authelia, oauth2-proxy, Traefik forward-auth, etc.) to authenticate users upstream and pass identity in HTTP headers. When enabled, Gitea Mirror reads the configured headers on each request and resolves the user automatically — no login form, no callback.
|
||||
|
||||
> **Important — operator-controlled by design.** Header auth is configured via environment variables only, not the UI. This is intentional: trusting a header means trusting whatever upstream sets it, and that decision belongs to the operator who controls the reverse proxy, not to a logged-in user inside the app. Make sure your reverse proxy strips these headers from inbound client requests, otherwise anyone can spoof them.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HEADER_AUTH_ENABLED` | Master switch — must be `true` to enable | `false` |
|
||||
| `HEADER_AUTH_USER_HEADER` | Header containing the username | `X-Authentik-Username` |
|
||||
| `HEADER_AUTH_EMAIL_HEADER` | Header containing the email address | `X-Authentik-Email` |
|
||||
| `HEADER_AUTH_NAME_HEADER` | Header containing the display name | `X-Authentik-Name` |
|
||||
| `HEADER_AUTH_AUTO_PROVISION` | If `true`, create a new user when an unknown username arrives. If `false`, unknown users are rejected. | `false` |
|
||||
| `HEADER_AUTH_ALLOWED_DOMAINS` | Comma-separated email domain allowlist. Empty = allow any. | _(empty)_ |
|
||||
|
||||
See also: [`docs/ENVIRONMENT_VARIABLES.md`](./ENVIRONMENT_VARIABLES.md#header-authentication-reverse-proxy-sso).
|
||||
|
||||
### Example: Docker Compose with Authentik
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ghcr.io/raylabshq/gitea-mirror:latest
|
||||
environment:
|
||||
HEADER_AUTH_ENABLED: "true"
|
||||
HEADER_AUTH_USER_HEADER: "X-Authentik-Username"
|
||||
HEADER_AUTH_EMAIL_HEADER: "X-Authentik-Email"
|
||||
HEADER_AUTH_NAME_HEADER: "X-Authentik-Name"
|
||||
HEADER_AUTH_AUTO_PROVISION: "true"
|
||||
HEADER_AUTH_ALLOWED_DOMAINS: "example.com,example.org"
|
||||
```
|
||||
|
||||
The defaults are Authentik-shaped, so for an Authentik proxy provider you generally only need to set `HEADER_AUTH_ENABLED=true` (and `HEADER_AUTH_AUTO_PROVISION=true` if you want first-login self-registration).
|
||||
|
||||
### Example: Authelia
|
||||
|
||||
Authelia uses different header names — override them:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
HEADER_AUTH_ENABLED: "true"
|
||||
HEADER_AUTH_USER_HEADER: "Remote-User"
|
||||
HEADER_AUTH_EMAIL_HEADER: "Remote-Email"
|
||||
HEADER_AUTH_NAME_HEADER: "Remote-Name"
|
||||
HEADER_AUTH_AUTO_PROVISION: "true"
|
||||
```
|
||||
|
||||
Then configure Authelia's `authz` rules to protect the gitea-mirror route and inject the `Remote-*` headers.
|
||||
|
||||
### Behaviour and lookup order
|
||||
|
||||
- The middleware checks for a cookie session **first**. Header auth only fires when there is no existing session, so users who logged in via password or SSO are not affected.
|
||||
- Lookup is by `HEADER_AUTH_USER_HEADER` value matched against `users.username`; if no match and `HEADER_AUTH_EMAIL_HEADER` is set, a second lookup is tried against `users.email`.
|
||||
- If neither matches:
|
||||
- With `HEADER_AUTH_AUTO_PROVISION=true`, a new user row is created (email defaults to `<username>@header-auth.local` if no email header is present).
|
||||
- With `HEADER_AUTH_AUTO_PROVISION=false`, the request is rejected with a warning logged.
|
||||
- If `HEADER_AUTH_ALLOWED_DOMAINS` is non-empty and the email header arrives, the email's domain must be in the list or the request is rejected.
|
||||
|
||||
### Verifying it's enabled
|
||||
|
||||
When header auth is active, the Authentication settings page renders a green "Header Authentication / Via reverse proxy" badge under the auth-methods status block. You can also probe `/api/auth/header-status`, which returns `{ "enabled": true, ... }` once the env vars are set.
|
||||
|
||||
### Security checklist
|
||||
|
||||
1. **Strip the headers at your edge.** Your reverse proxy must remove inbound `HEADER_AUTH_USER_HEADER` / email / name headers from client requests before re-injecting its own. Otherwise any unauthenticated client can set `X-Authentik-Username: admin` and walk in.
|
||||
2. **Bind the app to the proxy only.** Don't expose Gitea Mirror's port directly to the network when header auth is on; only the trusted proxy should reach it.
|
||||
3. **Use HTTPS between proxy and clients.** Headers travel in plaintext on the link they cross.
|
||||
4. **Be conservative with `HEADER_AUTH_AUTO_PROVISION`.** With it off, you provision users in the UI/DB once and the proxy fills in sessions thereafter — safer for shared deployments.
|
||||
|
||||
## Setting up OIDC Provider
|
||||
|
||||
The OIDC Provider feature allows other applications to use Gitea Mirror as their authentication provider.
|
||||
@@ -165,6 +274,8 @@ When an application requests authentication:
|
||||
1. **"Invalid origin" error**: Check that your Gitea Mirror URL matches the configured redirect URI
|
||||
2. **"Provider not found" error**: Ensure the provider is properly configured and enabled
|
||||
3. **Redirect loop**: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly
|
||||
4. **`?error=UNKNOWN` on the homepage after a successful upstream login** (or `?error=account%20not%20linked` in development): the SSO callback succeeded but Better Auth refused to link the SSO identity to an existing local account. The most common cause is the SSO provider's registered **Domain** not matching the user's actual email domain — see [Account Linking](#account-linking). Set `BETTER_AUTH_LOG_LEVEL=debug` to see Better Auth's full callback trace (look for "User already exist but account isn't linked to ...") and confirm the diagnosis. The same symptom in production gets sanitized to `UNKNOWN` by Better Auth's error page before the redirect, which is why the visible error is opaque.
|
||||
5. **`TypeError: undefined is not an object (evaluating 'url.startsWith')`**: This indicates the stored provider configuration is missing OIDC endpoints. Delete the provider from Gitea Mirror and re-register it using the **Discover** button so authorization/token URLs are saved (see [#73](https://github.com/RayLabsHQ/gitea-mirror/issues/73) and [#122](https://github.com/RayLabsHQ/gitea-mirror/issues/122) for examples).
|
||||
|
||||
### OIDC Provider Issues
|
||||
|
||||
@@ -202,4 +313,4 @@ This immediately prevents the application from authenticating new users.
|
||||
If migrating from the previous JWT-based authentication:
|
||||
- Existing users remain unaffected
|
||||
- Users can continue using email/password authentication
|
||||
- SSO can be added as an additional authentication method
|
||||
- SSO can be added as an additional authentication method
|
||||
@@ -125,11 +125,59 @@ npm start
|
||||
|
||||
### Debug Mode:
|
||||
|
||||
Enable debug logging by setting environment variable:
|
||||
> **Note:** Better Auth uses its own logger and does **not** read the `DEBUG`
|
||||
> environment variable (it is not based on the `debug` npm package). An older
|
||||
> version of this guide suggested `DEBUG=better-auth:*` — that has no effect.
|
||||
|
||||
Better Auth's logger defaults to the `warn` level, so SSO/OIDC sign-in and
|
||||
callback details are hidden. Set the log level to `debug` to surface the full
|
||||
trace:
|
||||
|
||||
```bash
|
||||
DEBUG=better-auth:* bun run dev
|
||||
# Local dev
|
||||
BETTER_AUTH_LOG_LEVEL=debug bun run dev
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Docker Compose
|
||||
services:
|
||||
gitea-mirror:
|
||||
environment:
|
||||
- BETTER_AUTH_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
Then watch the server logs (e.g. `docker compose logs -f gitea-mirror`) while you
|
||||
attempt an SSO login. Lines are prefixed with `[Better Auth]:`. Accepted values
|
||||
are `debug`, `info`, `warn`, and `error`.
|
||||
|
||||
#### Debugging a login that bounces back to `/login`
|
||||
|
||||
If clicking the SSO button sends you to the provider and then straight back to
|
||||
the login screen, the OAuth flow itself usually succeeded but **no session
|
||||
cookie was persisted**. Work through these checks:
|
||||
|
||||
1. **Enable `BETTER_AUTH_LOG_LEVEL=debug`** (above) and look for errors during
|
||||
the `/api/auth/sso/callback/<provider-id>` request.
|
||||
2. **Check for `?error=UNKNOWN` on the landing URL** (or `?error=account%20not%20linked` in dev).
|
||||
That's Better Auth's account-linking step refusing to attach the SSO identity
|
||||
to an existing email/password account. The debug log line to look for is
|
||||
`User already exist but account isn't linked to <providerId>`. The fix is
|
||||
almost always to set the SSO provider's **Domain** field to the email domain
|
||||
your users actually have — auto-linking is gated on that domain match.
|
||||
See [docs/SSO-OIDC-SETUP.md#account-linking](./SSO-OIDC-SETUP.md#account-linking).
|
||||
3. **Check the redirect URI** registered in your IdP exactly matches
|
||||
`https://<your-domain>/api/auth/sso/callback/<provider-id>` (scheme, host,
|
||||
and provider ID — no trailing slash).
|
||||
4. **Confirm the session cookie is set.** In the browser DevTools → Network,
|
||||
inspect the callback response for a `Set-Cookie: better-auth-session=…`
|
||||
header, and DevTools → Application → Cookies for the stored cookie. Behind a
|
||||
reverse proxy, ensure `BETTER_AUTH_URL` is your **external HTTPS** URL so the
|
||||
cookie is issued with the correct domain and `Secure` flag, and that the
|
||||
proxy forwards `X-Forwarded-Proto: https` and `X-Forwarded-Host`.
|
||||
5. **A `401` on `/api/sso/applications` is unrelated** to client login — that
|
||||
endpoint backs the OAuth *provider* (consent) management UI and requires an
|
||||
existing session. It is not part of the Authentik/OIDC sign-in flow.
|
||||
|
||||
## Testing Different Scenarios
|
||||
|
||||
### 1. New User Registration
|
||||
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -1,127 +0,0 @@
|
||||
# Testing in Gitea Mirror
|
||||
|
||||
This document provides guidance on testing in the Gitea Mirror project.
|
||||
|
||||
## Current Status
|
||||
|
||||
The project now uses Bun's built-in test runner, which is Jest-compatible and provides a fast, reliable testing experience. We've migrated away from Vitest due to compatibility issues with Bun.
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run tests, use the following commands:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run tests in watch mode (automatically re-run when files change)
|
||||
bun test --watch
|
||||
|
||||
# Run tests with coverage reporting
|
||||
bun test --coverage
|
||||
```
|
||||
|
||||
## Test File Naming Conventions
|
||||
|
||||
Bun's test runner automatically discovers test files that match the following patterns:
|
||||
|
||||
- `*.test.{js|jsx|ts|tsx}`
|
||||
- `*_test.{js|jsx|ts|tsx}`
|
||||
- `*.spec.{js|jsx|ts|tsx}`
|
||||
- `*_spec.{js|jsx|ts|tsx}`
|
||||
|
||||
## Writing Tests
|
||||
|
||||
The project uses Bun's test runner with a Jest-compatible API. Here's an example test:
|
||||
|
||||
```typescript
|
||||
// example.test.ts
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("Example Test", () => {
|
||||
test("should pass", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing React Components
|
||||
|
||||
For testing React components, we use React Testing Library:
|
||||
|
||||
```typescript
|
||||
// component.test.tsx
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import MyComponent from "../components/MyComponent";
|
||||
|
||||
describe("MyComponent", () => {
|
||||
test("renders correctly", () => {
|
||||
render(<MyComponent />);
|
||||
expect(screen.getByText("Hello World")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Setup
|
||||
|
||||
The test setup is defined in `src/tests/setup.bun.ts` and includes:
|
||||
|
||||
- Automatic cleanup after each test
|
||||
- Setup for any global test environment needs
|
||||
|
||||
## Mocking
|
||||
|
||||
Bun's test runner provides built-in mocking capabilities:
|
||||
|
||||
```typescript
|
||||
import { test, expect, mock } from "bun:test";
|
||||
|
||||
// Create a mock function
|
||||
const mockFn = mock(() => "mocked value");
|
||||
|
||||
test("mock function", () => {
|
||||
const result = mockFn();
|
||||
expect(result).toBe("mocked value");
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Mock a module
|
||||
mock.module("./some-module", () => {
|
||||
return {
|
||||
someFunction: () => "mocked module function"
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
The CI workflow has been updated to use Bun's test runner. Tests are automatically run as part of the CI pipeline.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
To generate test coverage reports, run:
|
||||
|
||||
```bash
|
||||
bun test --coverage
|
||||
```
|
||||
|
||||
This will generate a coverage report in the `coverage` directory.
|
||||
|
||||
## Types of Tests
|
||||
|
||||
The project includes several types of tests:
|
||||
|
||||
1. **Unit Tests**: Testing individual functions and utilities
|
||||
2. **API Tests**: Testing API endpoints
|
||||
3. **Component Tests**: Testing React components
|
||||
4. **Integration Tests**: Testing how components work together
|
||||
|
||||
## Future Improvements
|
||||
|
||||
When expanding the test suite, consider:
|
||||
|
||||
1. Adding more comprehensive API endpoint tests
|
||||
2. Increasing component test coverage
|
||||
3. Setting up end-to-end tests with a tool like Playwright
|
||||
4. Adding performance tests for critical paths
|
||||
@@ -1 +1,11 @@
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_full_name` ON `repositories` (`user_id`,`full_name`);
|
||||
-- Step 1: Remove duplicate repositories, keeping the most recently updated one
|
||||
-- This handles cases where users have duplicate entries from before the unique constraint
|
||||
DELETE FROM repositories
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MAX(rowid)
|
||||
FROM repositories
|
||||
GROUP BY user_id, full_name
|
||||
);
|
||||
--> statement-breakpoint
|
||||
-- Step 2: Now create the unique index safely
|
||||
CREATE UNIQUE INDEX uniq_repositories_user_full_name ON repositories (user_id, full_name);
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `accounts` ADD `id_token` text;--> statement-breakpoint
|
||||
ALTER TABLE `accounts` ADD `access_token_expires_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `accounts` ADD `refresh_token_expires_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `accounts` ADD `scope` text;
|
||||
@@ -0,0 +1,18 @@
|
||||
ALTER TABLE `organizations` ADD `normalized_name` text NOT NULL DEFAULT '';--> statement-breakpoint
|
||||
UPDATE `organizations` SET `normalized_name` = lower(trim(`name`));--> statement-breakpoint
|
||||
DELETE FROM `organizations`
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM `organizations`
|
||||
GROUP BY `user_id`, `normalized_name`
|
||||
);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_organizations_user_normalized_name` ON `organizations` (`user_id`,`normalized_name`);--> statement-breakpoint
|
||||
ALTER TABLE `repositories` ADD `normalized_full_name` text NOT NULL DEFAULT '';--> statement-breakpoint
|
||||
UPDATE `repositories` SET `normalized_full_name` = lower(trim(`full_name`));--> statement-breakpoint
|
||||
DELETE FROM `repositories`
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM `repositories`
|
||||
GROUP BY `user_id`, `normalized_full_name`
|
||||
);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_normalized_full_name` ON `repositories` (`user_id`,`normalized_full_name`);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `repositories` ADD `metadata` text;
|
||||
@@ -0,0 +1,149 @@
|
||||
CREATE TABLE `__new_repositories` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`config_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`full_name` text NOT NULL,
|
||||
`normalized_full_name` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`clone_url` text NOT NULL,
|
||||
`owner` text NOT NULL,
|
||||
`organization` text,
|
||||
`mirrored_location` text DEFAULT '',
|
||||
`is_private` integer DEFAULT false NOT NULL,
|
||||
`is_fork` integer DEFAULT false NOT NULL,
|
||||
`forked_from` text,
|
||||
`has_issues` integer DEFAULT false NOT NULL,
|
||||
`is_starred` integer DEFAULT false NOT NULL,
|
||||
`is_archived` integer DEFAULT false NOT NULL,
|
||||
`size` integer DEFAULT 0 NOT NULL,
|
||||
`has_lfs` integer DEFAULT false NOT NULL,
|
||||
`has_submodules` integer DEFAULT false NOT NULL,
|
||||
`language` text,
|
||||
`description` text,
|
||||
`default_branch` text NOT NULL,
|
||||
`visibility` text DEFAULT 'public' NOT NULL,
|
||||
`status` text DEFAULT 'imported' NOT NULL,
|
||||
`last_mirrored` integer,
|
||||
`error_message` text,
|
||||
`destination_org` text,
|
||||
`metadata` text,
|
||||
`imported_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_repositories` (
|
||||
`id`,
|
||||
`user_id`,
|
||||
`config_id`,
|
||||
`name`,
|
||||
`full_name`,
|
||||
`normalized_full_name`,
|
||||
`url`,
|
||||
`clone_url`,
|
||||
`owner`,
|
||||
`organization`,
|
||||
`mirrored_location`,
|
||||
`is_private`,
|
||||
`is_fork`,
|
||||
`forked_from`,
|
||||
`has_issues`,
|
||||
`is_starred`,
|
||||
`is_archived`,
|
||||
`size`,
|
||||
`has_lfs`,
|
||||
`has_submodules`,
|
||||
`language`,
|
||||
`description`,
|
||||
`default_branch`,
|
||||
`visibility`,
|
||||
`status`,
|
||||
`last_mirrored`,
|
||||
`error_message`,
|
||||
`destination_org`,
|
||||
`metadata`,
|
||||
`imported_at`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
)
|
||||
SELECT
|
||||
`repositories`.`id`,
|
||||
`repositories`.`user_id`,
|
||||
`repositories`.`config_id`,
|
||||
`repositories`.`name`,
|
||||
`repositories`.`full_name`,
|
||||
`repositories`.`normalized_full_name`,
|
||||
`repositories`.`url`,
|
||||
`repositories`.`clone_url`,
|
||||
`repositories`.`owner`,
|
||||
`repositories`.`organization`,
|
||||
`repositories`.`mirrored_location`,
|
||||
`repositories`.`is_private`,
|
||||
`repositories`.`is_fork`,
|
||||
`repositories`.`forked_from`,
|
||||
`repositories`.`has_issues`,
|
||||
`repositories`.`is_starred`,
|
||||
`repositories`.`is_archived`,
|
||||
`repositories`.`size`,
|
||||
`repositories`.`has_lfs`,
|
||||
`repositories`.`has_submodules`,
|
||||
`repositories`.`language`,
|
||||
`repositories`.`description`,
|
||||
`repositories`.`default_branch`,
|
||||
`repositories`.`visibility`,
|
||||
`repositories`.`status`,
|
||||
`repositories`.`last_mirrored`,
|
||||
`repositories`.`error_message`,
|
||||
`repositories`.`destination_org`,
|
||||
`repositories`.`metadata`,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT MIN(`mj`.`timestamp`)
|
||||
FROM `mirror_jobs` `mj`
|
||||
WHERE `mj`.`user_id` = `repositories`.`user_id`
|
||||
AND `mj`.`status` = 'imported'
|
||||
AND (
|
||||
(`mj`.`repository_id` IS NOT NULL AND `mj`.`repository_id` = `repositories`.`id`)
|
||||
OR (
|
||||
`mj`.`repository_id` IS NULL
|
||||
AND `mj`.`repository_name` IS NOT NULL
|
||||
AND (
|
||||
lower(trim(`mj`.`repository_name`)) = `repositories`.`normalized_full_name`
|
||||
OR lower(trim(`mj`.`repository_name`)) = lower(trim(`repositories`.`name`))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
`repositories`.`created_at`,
|
||||
unixepoch()
|
||||
) AS `imported_at`,
|
||||
`repositories`.`created_at`,
|
||||
`repositories`.`updated_at`
|
||||
FROM `repositories`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `repositories`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `__new_repositories` RENAME TO `repositories`;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_user_id` ON `repositories` (`user_id`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_config_id` ON `repositories` (`config_id`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_status` ON `repositories` (`status`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_owner` ON `repositories` (`owner`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_organization` ON `repositories` (`organization`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_is_fork` ON `repositories` (`is_fork`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_is_starred` ON `repositories` (`is_starred`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_repositories_user_imported_at` ON `repositories` (`user_id`,`imported_at`);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_full_name` ON `repositories` (`user_id`,`full_name`);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_normalized_full_name` ON `repositories` (`user_id`,`normalized_full_name`);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add index for mirroredLocation lookups (used by name collision detection)
|
||||
CREATE INDEX IF NOT EXISTS `idx_repositories_mirrored_location` ON `repositories` (`user_id`, `mirrored_location`);
|
||||
|
||||
-- Add unique partial index to enforce that no two repos for the same user
|
||||
-- can claim the same non-empty mirroredLocation. This prevents race conditions
|
||||
-- during concurrent batch mirroring of starred repos with duplicate names.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `uniq_repositories_user_mirrored_location`
|
||||
ON `repositories` (`user_id`, `mirrored_location`)
|
||||
WHERE `mirrored_location` != '';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `configs` ADD `notification_config` text DEFAULT '{"enabled":false,"provider":"ntfy","notifyOnSyncError":true,"notifyOnSyncSuccess":false,"notifyOnNewRepo":false}' NOT NULL;
|
||||
@@ -0,0 +1,126 @@
|
||||
-- Migrate the OAuth/OIDC *provider* feature from the deprecated
|
||||
-- `oidc-provider` plugin to `@better-auth/oauth-provider`.
|
||||
--
|
||||
-- Tables: oauth_applications -> oauth_clients, oauth_access_tokens reshaped,
|
||||
-- new oauth_refresh_tokens, oauth_consent -> oauth_consents, plus a `jwks`
|
||||
-- table for the `jwt` plugin (id_token signing keys).
|
||||
--
|
||||
-- Data preservation:
|
||||
-- * Registered clients are copied from oauth_applications into oauth_clients,
|
||||
-- converting the legacy comma-separated `redirect_urls` into the JSON
|
||||
-- string[] (`redirect_uris`) the new adapter expects.
|
||||
-- * Access tokens and consent records are NOT migrated: access tokens are
|
||||
-- short-lived (and the column shape changed entirely), and consents are
|
||||
-- cheaply re-granted on next authorization. Both old tables are dropped.
|
||||
--
|
||||
-- NOTE: legacy client secrets were stored in plaintext, whereas the new
|
||||
-- provider stores them hashed. Migrated client secrets will therefore not
|
||||
-- validate as-is — affected applications must rotate their secret after
|
||||
-- upgrade.
|
||||
|
||||
CREATE TABLE `oauth_clients` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`client_secret` text,
|
||||
`name` text,
|
||||
`disabled` integer DEFAULT false,
|
||||
`skip_consent` integer,
|
||||
`enable_end_session` integer,
|
||||
`subject_type` text,
|
||||
`scopes` text,
|
||||
`user_id` text,
|
||||
`uri` text,
|
||||
`icon` text,
|
||||
`contacts` text,
|
||||
`tos` text,
|
||||
`policy` text,
|
||||
`software_id` text,
|
||||
`software_version` text,
|
||||
`software_statement` text,
|
||||
`redirect_uris` text NOT NULL,
|
||||
`post_logout_redirect_uris` text,
|
||||
`token_endpoint_auth_method` text,
|
||||
`grant_types` text,
|
||||
`response_types` text,
|
||||
`public` integer,
|
||||
`type` text,
|
||||
`require_pkce` integer,
|
||||
`reference_id` text,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT (unixepoch()),
|
||||
`updated_at` integer DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `oauth_clients_client_id_unique` ON `oauth_clients` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_clients_client_id` ON `oauth_clients` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_clients_user_id` ON `oauth_clients` (`user_id`);--> statement-breakpoint
|
||||
INSERT INTO `oauth_clients` (
|
||||
`id`, `client_id`, `client_secret`, `name`, `disabled`, `user_id`,
|
||||
`redirect_uris`, `type`, `metadata`, `created_at`, `updated_at`
|
||||
)
|
||||
SELECT
|
||||
`id`, `client_id`, `client_secret`, `name`, `disabled`, `user_id`,
|
||||
'["' || replace(`redirect_urls`, ',', '","') || '"]',
|
||||
`type`, `metadata`, `created_at`, `updated_at`
|
||||
FROM `oauth_applications`;--> statement-breakpoint
|
||||
DROP TABLE `oauth_applications`;--> statement-breakpoint
|
||||
DROP TABLE `oauth_access_tokens`;--> statement-breakpoint
|
||||
CREATE TABLE `oauth_access_tokens` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`token` text,
|
||||
`client_id` text NOT NULL,
|
||||
`session_id` text,
|
||||
`user_id` text,
|
||||
`reference_id` text,
|
||||
`refresh_id` text,
|
||||
`expires_at` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()),
|
||||
`scopes` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `oauth_access_tokens_token_unique` ON `oauth_access_tokens` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_access_tokens_token` ON `oauth_access_tokens` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_access_tokens_client_id` ON `oauth_access_tokens` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_access_tokens_user_id` ON `oauth_access_tokens` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `oauth_refresh_tokens` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`session_id` text,
|
||||
`user_id` text NOT NULL,
|
||||
`reference_id` text,
|
||||
`expires_at` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()),
|
||||
`revoked` integer,
|
||||
`auth_time` integer,
|
||||
`scopes` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `oauth_refresh_tokens_token_unique` ON `oauth_refresh_tokens` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_refresh_tokens_token` ON `oauth_refresh_tokens` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_refresh_tokens_client_id` ON `oauth_refresh_tokens` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_refresh_tokens_user_id` ON `oauth_refresh_tokens` (`user_id`);--> statement-breakpoint
|
||||
DROP TABLE `oauth_consent`;--> statement-breakpoint
|
||||
CREATE TABLE `oauth_consents` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`user_id` text,
|
||||
`reference_id` text,
|
||||
`scopes` text NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()),
|
||||
`updated_at` integer DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_consents_client_id` ON `oauth_consents` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_consents_user_id` ON `oauth_consents` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `jwks` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`public_key` text NOT NULL,
|
||||
`private_key` text NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`expires_at` integer
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_organizations` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`config_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`normalized_name` text NOT NULL,
|
||||
`avatar_url` text NOT NULL,
|
||||
`membership_role` text DEFAULT 'member' NOT NULL,
|
||||
`is_included` integer DEFAULT true NOT NULL,
|
||||
`destination_org` text,
|
||||
`status` text DEFAULT 'imported' NOT NULL,
|
||||
`last_mirrored` integer,
|
||||
`error_message` text,
|
||||
`repository_count` integer DEFAULT 0 NOT NULL,
|
||||
`public_repository_count` integer,
|
||||
`private_repository_count` integer,
|
||||
`fork_repository_count` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`config_id`) REFERENCES `configs`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_organizations`("id", "user_id", "config_id", "name", "normalized_name", "avatar_url", "membership_role", "is_included", "destination_org", "status", "last_mirrored", "error_message", "repository_count", "public_repository_count", "private_repository_count", "fork_repository_count", "created_at", "updated_at") SELECT "id", "user_id", "config_id", "name", "normalized_name", "avatar_url", "membership_role", "is_included", "destination_org", "status", "last_mirrored", "error_message", "repository_count", "public_repository_count", "private_repository_count", "fork_repository_count", "created_at", "updated_at" FROM `organizations`;--> statement-breakpoint
|
||||
DROP TABLE `organizations`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_organizations` RENAME TO `organizations`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE INDEX `idx_organizations_user_id` ON `organizations` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_organizations_config_id` ON `organizations` (`config_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_organizations_status` ON `organizations` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_organizations_is_included` ON `organizations` (`is_included`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_organizations_user_normalized_name` ON `organizations` (`user_id`,`normalized_name`);--> statement-breakpoint
|
||||
ALTER TABLE `sso_providers` ADD `saml_config` text;--> statement-breakpoint
|
||||
ALTER TABLE `sso_providers` ADD `domain_verified` integer DEFAULT true NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `organizations` ADD `mirror_overrides` text;--> statement-breakpoint
|
||||
ALTER TABLE `repositories` ADD `mirror_overrides` text;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `repositories` ADD `source_provider` text DEFAULT 'github' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `repositories` ADD `source_url` text DEFAULT 'https://github.com' NOT NULL;
|
||||
@@ -0,0 +1,63 @@
|
||||
CREATE TABLE `oauth_client_assertions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`expires_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `oauth_client_resources` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`resource_id` text NOT NULL,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT (unixepoch())
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_client_resources_client_id` ON `oauth_client_resources` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_client_resources_resource_id` ON `oauth_client_resources` (`resource_id`);--> statement-breakpoint
|
||||
CREATE TABLE `oauth_resources` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`access_token_ttl` integer,
|
||||
`refresh_token_ttl` integer,
|
||||
`signing_algorithm` text,
|
||||
`signing_key_id` text,
|
||||
`allowed_scopes` text,
|
||||
`custom_claims` text,
|
||||
`dpop_bound_access_tokens_required` integer,
|
||||
`disabled` integer,
|
||||
`policy_version` integer,
|
||||
`metadata` text,
|
||||
`created_at` integer DEFAULT (unixepoch()),
|
||||
`updated_at` integer DEFAULT (unixepoch())
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `oauth_resources_identifier_unique` ON `oauth_resources` (`identifier`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_oauth_resources_identifier` ON `oauth_resources` (`identifier`);--> statement-breakpoint
|
||||
ALTER TABLE `accounts` ADD `issuer` text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `jwks` ADD `alg` text;--> statement-breakpoint
|
||||
ALTER TABLE `jwks` ADD `crv` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_access_tokens` ADD `authorization_code_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_access_tokens` ADD `resources` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_access_tokens` ADD `requested_user_info_claims` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_access_tokens` ADD `revoked` integer;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_access_tokens` ADD `confirmation` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `client_discovery_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `client_credentials_scopes` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `backchannel_logout_uri` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `backchannel_logout_session_required` integer;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `application_type` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `jwks` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `jwks_uri` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_clients` ADD `dpop_bound_access_tokens` integer;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_consents` ADD `resources` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_consents` ADD `requested_user_info_claims` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `authorization_code_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `resources` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `requested_user_info_claims` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `rotated_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `rotation_replay_response` text;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `rotation_replay_expires_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `oauth_refresh_tokens` ADD `confirmation` text;--> statement-breakpoint
|
||||
UPDATE `accounts` SET `issuer` = 'local:credential' WHERE `issuer` = '' AND `provider_id` = 'credential';--> statement-breakpoint
|
||||
UPDATE `accounts` SET `issuer` = (SELECT `issuer` FROM `sso_providers` WHERE `sso_providers`.`provider_id` = `accounts`.`provider_id` LIMIT 1) WHERE `issuer` = '' AND EXISTS (SELECT 1 FROM `sso_providers` WHERE `sso_providers`.`provider_id` = `accounts`.`provider_id`);--> statement-breakpoint
|
||||
UPDATE `accounts` SET `issuer` = 'local:oauth:' || `provider_id` WHERE `issuer` = '';
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE `api_keys` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`config_id` text DEFAULT 'default' NOT NULL,
|
||||
`name` text,
|
||||
`start` text,
|
||||
`prefix` text,
|
||||
`key` text NOT NULL,
|
||||
`reference_id` text NOT NULL,
|
||||
`refill_interval` integer,
|
||||
`refill_amount` integer,
|
||||
`last_refill_at` integer,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`rate_limit_enabled` integer DEFAULT true NOT NULL,
|
||||
`rate_limit_time_window` integer,
|
||||
`rate_limit_max` integer,
|
||||
`request_count` integer DEFAULT 0 NOT NULL,
|
||||
`remaining` integer,
|
||||
`last_request` integer,
|
||||
`expires_at` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`permissions` text,
|
||||
`metadata` text,
|
||||
FOREIGN KEY (`reference_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_api_keys_reference_id` ON `api_keys` (`reference_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_api_keys_key` ON `api_keys` (`key`);
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `repositories` ADD `destination_provider` text DEFAULT 'gitea' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `repositories` ADD `destination_url` text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||
UPDATE `repositories` SET
|
||||
`destination_provider` = COALESCE((SELECT json_extract(`configs`.`gitea_config`, '$.provider') FROM `configs` WHERE `configs`.`id` = `repositories`.`config_id`), 'gitea'),
|
||||
`destination_url` = COALESCE((SELECT json_extract(`configs`.`gitea_config`, '$.url') FROM `configs` WHERE `configs`.`id` = `repositories`.`config_id`), '')
|
||||
WHERE `destination_url` = '';--> statement-breakpoint
|
||||
UPDATE `repositories` SET `destination_provider` = 'gitea' WHERE `destination_provider` NOT IN ('gitea', 'forgejo', 'github', 'gitlab');
|
||||
@@ -0,0 +1,82 @@
|
||||
CREATE TABLE `sources` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`provider` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`username` text DEFAULT '' NOT NULL,
|
||||
`token` text,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_sources_user_id` ON `sources` (`user_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_sources_user_provider_url_username` ON `sources` (`user_id`,`provider`,`url`,`username`);--> statement-breakpoint
|
||||
-- Seed one source row per ACTIVE config that carries connection fields. The
|
||||
-- token is copied as-is: it is already encrypted at rest in the config.
|
||||
-- INSERT OR IGNORE guards the unique index for accounts that somehow kept
|
||||
-- multiple config rows with identical connection values (configs has no
|
||||
-- per-user unique constraint). Active configs only: the app reads the
|
||||
-- active config, so seeding from a stale inactive row would elect a primary
|
||||
-- source the user no longer uses.
|
||||
INSERT OR IGNORE INTO `sources` (`id`, `user_id`, `name`, `provider`, `url`, `username`, `token`, `enabled`, `created_at`, `updated_at`)
|
||||
WITH `connection` AS (
|
||||
SELECT
|
||||
`configs`.`user_id` AS `user_id`,
|
||||
CASE json_extract(`configs`.`github_config`, '$.provider')
|
||||
WHEN 'gitlab' THEN 'gitlab'
|
||||
WHEN 'gitea' THEN 'gitea'
|
||||
WHEN 'forgejo' THEN 'gitea'
|
||||
WHEN 'codeberg' THEN 'gitea'
|
||||
WHEN 'github' THEN 'github'
|
||||
ELSE 'github'
|
||||
END AS `provider`,
|
||||
rtrim(COALESCE(json_extract(`configs`.`github_config`, '$.url'), ''), '/') AS `url`,
|
||||
COALESCE(json_extract(`configs`.`github_config`, '$.owner'), '') AS `username`,
|
||||
COALESCE(json_extract(`configs`.`github_config`, '$.token'), '') AS `token`
|
||||
FROM `configs`
|
||||
WHERE `configs`.`is_active` = 1
|
||||
AND (
|
||||
COALESCE(json_extract(`configs`.`github_config`, '$.token'), '') <> ''
|
||||
OR COALESCE(json_extract(`configs`.`github_config`, '$.owner'), '') <> ''
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(6))),
|
||||
`user_id`,
|
||||
CASE `provider`
|
||||
WHEN 'github' THEN 'GitHub'
|
||||
WHEN 'gitlab' THEN 'GitLab'
|
||||
ELSE 'Gitea / Forgejo'
|
||||
END || (CASE WHEN `username` = '' THEN '' ELSE ' (' || `username` || ')' END),
|
||||
`provider`,
|
||||
CASE
|
||||
WHEN `url` <> '' THEN `url`
|
||||
WHEN `provider` = 'github' THEN 'https://github.com'
|
||||
WHEN `provider` = 'gitlab' THEN 'https://gitlab.com'
|
||||
ELSE 'https://codeberg.org'
|
||||
END,
|
||||
`username`,
|
||||
NULLIF(`token`, ''),
|
||||
1,
|
||||
unixepoch(),
|
||||
unixepoch()
|
||||
FROM `connection`;--> statement-breakpoint
|
||||
ALTER TABLE `repositories` ADD `source_id` text;--> statement-breakpoint
|
||||
-- Link each repository to its user's source row with the same provider and
|
||||
-- host. MIN() keeps the subquery scalar; rows without a match keep NULL
|
||||
-- ("switched source" legacy rows that deliberately match no source).
|
||||
UPDATE `repositories` SET `source_id` = (
|
||||
SELECT MIN(`sources`.`id`)
|
||||
FROM `sources`
|
||||
WHERE `sources`.`user_id` = `repositories`.`user_id`
|
||||
AND `sources`.`provider` = `repositories`.`source_provider`
|
||||
AND rtrim(`sources`.`url`, '/') = rtrim(`repositories`.`source_url`, '/')
|
||||
)
|
||||
WHERE `source_id` IS NULL;--> statement-breakpoint
|
||||
DROP INDEX `uniq_repositories_user_full_name`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_full_name` ON `repositories` (`user_id`,`source_id`,`full_name`);--> statement-breakpoint
|
||||
DROP INDEX `uniq_repositories_user_normalized_full_name`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_normalized_full_name` ON `repositories` (`user_id`,`source_id`,`normalized_full_name`);
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE `organizations` ADD `source_id` text;--> statement-breakpoint
|
||||
-- Pin each organization to the source its repositories came from, but only
|
||||
-- when they agree on exactly one: organizations whose repos span several
|
||||
-- sources (or have none) stay unpinned and keep following every source.
|
||||
-- MIN() keeps the subquery scalar; a HAVING filter with no matching row
|
||||
-- yields NULL, which leaves the column untouched.
|
||||
UPDATE `organizations` SET `source_id` = (
|
||||
SELECT MIN(`repositories`.`source_id`)
|
||||
FROM `repositories`
|
||||
WHERE `repositories`.`user_id` = `organizations`.`user_id`
|
||||
AND `repositories`.`organization` = `organizations`.`name`
|
||||
AND `repositories`.`source_id` IS NOT NULL
|
||||
HAVING COUNT(DISTINCT `repositories`.`source_id`) = 1
|
||||
)
|
||||
WHERE `source_id` IS NULL;
|
||||