diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8537a5dfd..432841f64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,100 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} - name: Check files uses: pre-commit/action@v3.0.1 + verify-environment: + name: "Verify Environment & Services" + runs-on: ubuntu-24.04 + needs: + - pre-commit + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Verify Docker installation + run: | + if ! command -v docker &> /dev/null; then + echo "✗ Docker is not installed" + exit 1 + fi + echo "✓ Docker is installed: $(docker --version)" + - name: Verify docker-compose installation + run: | + if ! command -v docker &> /dev/null || ! docker compose version &> /dev/null; then + echo "✗ docker-compose is not installed" + exit 1 + fi + echo "✓ docker compose is installed: $(docker compose version)" + - name: Verify compose file exists + env: + COMPOSE_FILE: docker/compose/docker-compose.intellidocs.yml + run: | + if [ ! -f "$COMPOSE_FILE" ]; then + echo "✗ Compose file not found: $COMPOSE_FILE" + exit 1 + fi + echo "✓ Compose file found: $COMPOSE_FILE" + - name: Set up Python + id: setup-python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: ${{ env.DEFAULT_UV_VERSION }} + enable-cache: true + python-version: ${{ steps.setup-python.outputs.python-version }} + - name: Generate and verify requirements.txt + run: | + uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt + if [ ! -f "requirements.txt" ]; then + echo "✗ requirements.txt was not generated" + exit 1 + fi + echo "✓ requirements.txt generated successfully" + - name: Verify Python dependencies installation + run: | + # Verify that requirements.txt can be parsed + if ! python -c " +import sys +try: + with open('requirements.txt', 'r') as f: + lines = f.readlines() + print(f'✓ requirements.txt has {len(lines)} entries') + sys.exit(0) +except Exception as e: + print(f'✗ Error reading requirements.txt: {e}') + sys.exit(1) + "; then + exit 1 + fi + - name: Start Redis service + run: | + docker compose --file docker/compose/docker-compose.intellidocs.yml up -d broker + echo "Waiting for Redis to be ready..." + sleep 10 + - name: Verify Redis is responding + run: | + MAX_RETRIES=30 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if docker compose --file docker/compose/docker-compose.intellidocs.yml exec -T broker redis-cli ping &> /dev/null; then + echo "✓ Redis is responding" + exit 0 + fi + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Waiting for Redis... (attempt $RETRY_COUNT/$MAX_RETRIES)" + sleep 2 + done + + echo "✗ Redis is not responding after $MAX_RETRIES attempts" + docker compose --file docker/compose/docker-compose.intellidocs.yml logs broker + exit 1 + - name: Stop services + if: always() + run: | + docker compose --file docker/compose/docker-compose.intellidocs.yml down -v documentation: name: "Build & Deploy Documentation" runs-on: ubuntu-24.04 diff --git a/.github/workflows/docker-intellidocs.yml b/.github/workflows/docker-intellidocs.yml index 53c726e6a..ab989da24 100644 --- a/.github/workflows/docker-intellidocs.yml +++ b/.github/workflows/docker-intellidocs.yml @@ -25,10 +25,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -242,7 +242,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Create Release uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/pr-bot.yml b/.github/workflows/pr-bot.yml index 8e3b7951d..83bb86849 100644 --- a/.github/workflows/pr-bot.yml +++ b/.github/workflows/pr-bot.yml @@ -83,11 +83,18 @@ jobs: const pr = context.payload.pull_request; const user = pr.user.login; - const { data: members } = await github.rest.orgs.listMembers({ - org: 'paperless-ngx', - }); + // Try to get org members, but handle if this is not an org + let memberLogins = []; + try { + const { data: members } = await github.rest.orgs.listMembers({ + org: context.repo.owner, + }); + memberLogins = members.map(m => m.login.toLowerCase()); + } catch (error) { + // If not an organization, only skip for repo collaborators + core.info('Not an organization or unable to fetch members'); + } - const memberLogins = members.map(m => m.login.toLowerCase()); if (memberLogins.includes(user.toLowerCase())) { core.info('Skipping comment: user is org member'); return; @@ -98,7 +105,7 @@ jobs: "Thank you very much for submitting this PR to us!\n\n" + "This is what will happen next:\n\n" + "1. CI tests will run against your PR to ensure quality and consistency.\n" + - "2. Next, human contributors from paperless-ngx review your changes.\n" + + "2. Next, human contributors will review your changes.\n" + "3. Please address any issues that come up during the review as soon as you are able to.\n" + "4. If accepted, your pull request will be merged into the `dev` branch and changes there will be tested further.\n" + "5. Eventually, changes from you and other contributors will be merged into `main` and a new release will be made.\n\n" + diff --git a/.github/workflows/translate-strings.yml b/.github/workflows/translate-strings.yml index f94191f19..43f993221 100644 --- a/.github/workflows/translate-strings.yml +++ b/.github/workflows/translate-strings.yml @@ -63,6 +63,7 @@ jobs: - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v6 with: + token: ${{ secrets.GITHUB_TOKEN }} file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po' commit_message: "Auto translate strings" commit_user_name: "GitHub Actions" diff --git a/src-ui/src/app/components/ai-suggestions-panel/ai-suggestions-panel.component.ts b/src-ui/src/app/components/ai-suggestions-panel/ai-suggestions-panel.component.ts index b55b881c8..a1a91c2ff 100644 --- a/src-ui/src/app/components/ai-suggestions-panel/ai-suggestions-panel.component.ts +++ b/src-ui/src/app/components/ai-suggestions-panel/ai-suggestions-panel.component.ts @@ -18,8 +18,8 @@ import { } from '@angular/core' import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' -import { Subject } from 'rxjs' -import { takeUntil } from 'rxjs/operators' +import { Subject, of } from 'rxjs' +import { takeUntil, catchError } from 'rxjs/operators' import { AISuggestion, AISuggestionStatus, @@ -134,10 +134,19 @@ export class AiSuggestionsPanelComponent implements OnChanges, OnDestroy { (s) => s.type === AISuggestionType.Tag ) if (tagSuggestions.length > 0) { - this.tagService.listAll().pipe(takeUntil(this.destroy$)).subscribe((tags) => { - this.tags = tags.results - this.updateSuggestionLabels() - }) + this.tagService + .listAll() + .pipe( + takeUntil(this.destroy$), + catchError((error) => { + console.error('Failed to load tags:', error) + return of({ results: [] }) + }) + ) + .subscribe((tags) => { + this.tags = tags.results + this.updateSuggestionLabels() + }) } // Load correspondents if needed @@ -145,10 +154,19 @@ export class AiSuggestionsPanelComponent implements OnChanges, OnDestroy { (s) => s.type === AISuggestionType.Correspondent ) if (correspondentSuggestions.length > 0) { - this.correspondentService.listAll().pipe(takeUntil(this.destroy$)).subscribe((correspondents) => { - this.correspondents = correspondents.results - this.updateSuggestionLabels() - }) + this.correspondentService + .listAll() + .pipe( + takeUntil(this.destroy$), + catchError((error) => { + console.error('Failed to load correspondents:', error) + return of({ results: [] }) + }) + ) + .subscribe((correspondents) => { + this.correspondents = correspondents.results + this.updateSuggestionLabels() + }) } // Load document types if needed @@ -156,10 +174,19 @@ export class AiSuggestionsPanelComponent implements OnChanges, OnDestroy { (s) => s.type === AISuggestionType.DocumentType ) if (documentTypeSuggestions.length > 0) { - this.documentTypeService.listAll().pipe(takeUntil(this.destroy$)).subscribe((documentTypes) => { - this.documentTypes = documentTypes.results - this.updateSuggestionLabels() - }) + this.documentTypeService + .listAll() + .pipe( + takeUntil(this.destroy$), + catchError((error) => { + console.error('Failed to load document types:', error) + return of({ results: [] }) + }) + ) + .subscribe((documentTypes) => { + this.documentTypes = documentTypes.results + this.updateSuggestionLabels() + }) } // Load storage paths if needed @@ -167,10 +194,19 @@ export class AiSuggestionsPanelComponent implements OnChanges, OnDestroy { (s) => s.type === AISuggestionType.StoragePath ) if (storagePathSuggestions.length > 0) { - this.storagePathService.listAll().pipe(takeUntil(this.destroy$)).subscribe((storagePaths) => { - this.storagePaths = storagePaths.results - this.updateSuggestionLabels() - }) + this.storagePathService + .listAll() + .pipe( + takeUntil(this.destroy$), + catchError((error) => { + console.error('Failed to load storage paths:', error) + return of({ results: [] }) + }) + ) + .subscribe((storagePaths) => { + this.storagePaths = storagePaths.results + this.updateSuggestionLabels() + }) } // Load custom fields if needed @@ -178,10 +214,19 @@ export class AiSuggestionsPanelComponent implements OnChanges, OnDestroy { (s) => s.type === AISuggestionType.CustomField ) if (customFieldSuggestions.length > 0) { - this.customFieldsService.listAll().pipe(takeUntil(this.destroy$)).subscribe((customFields) => { - this.customFields = customFields.results - this.updateSuggestionLabels() - }) + this.customFieldsService + .listAll() + .pipe( + takeUntil(this.destroy$), + catchError((error) => { + console.error('Failed to load custom fields:', error) + return of({ results: [] }) + }) + ) + .subscribe((customFields) => { + this.customFields = customFields.results + this.updateSuggestionLabels() + }) } } diff --git a/src-ui/src/app/services/ai-status.service.ts b/src-ui/src/app/services/ai-status.service.ts index 569ad337c..bdc1c4fd4 100644 --- a/src-ui/src/app/services/ai-status.service.ts +++ b/src-ui/src/app/services/ai-status.service.ts @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http' import { Injectable, inject } from '@angular/core' -import { BehaviorSubject, Observable, interval, Subscription } from 'rxjs' +import { BehaviorSubject, Observable, interval, Subscription, of } from 'rxjs' import { catchError, map, startWith, switchMap } from 'rxjs/operators' import { AIStatus } from 'src/app/data/ai-status' import { environment } from 'src/environments/environment' @@ -86,19 +86,15 @@ export class AIStatusService { }), catchError((error) => { this.loading = false - console.warn('Failed to fetch AI status, using mock data:', error) - // Return mock data if endpoint doesn't exist yet - return [ - { - active: true, - processing: false, - documents_scanned_today: 42, - suggestions_applied: 15, - pending_deletion_requests: 2, - last_scan: new Date().toISOString(), - version: '1.0.0', - }, - ] + console.warn('Failed to fetch AI status:', error) + // Return default status if endpoint fails + return of({ + active: false, + processing: false, + documents_scanned_today: 0, + suggestions_applied: 0, + pending_deletion_requests: 0, + }) }) ) }