mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			550e74e559
			...
			fix-chore-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 770fb2d60e | ||
|   | c8ef9e663a | ||
|   | 2195e4af45 | ||
|   | c6716905a4 | ||
|   | 850ee5a415 | ||
|   | b25b5abdb0 | ||
|   | 68e0559053 | ||
|   | 4ff09c4cf4 | ||
|   | 53b393dab5 | ||
|   | 6119c215e7 | ||
|   | 8d1f23e9d6 | ||
|   | c8850fa752 | ||
|   | 19a54b3b23 | ||
|   | 1cdd8d9ba8 | ||
|   | 4449dbadb5 | ||
|   | 43b4f36026 | ||
|   | 0e35acaef5 | ||
|   | 19ff339804 | ||
|   | 6b868a5ecb | ||
|   | 3e4aa87cc5 | ||
|   | fc95d42b35 | 
							
								
								
									
										430
									
								
								.github/workflows/build-and-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										430
									
								
								.github/workflows/build-and-release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,430 @@ | |||||||
|  | name: 'Build and Release' | ||||||
|  | on: | ||||||
|  |   workflow_run: | ||||||
|  |     workflows: | ||||||
|  |       - ci | ||||||
|  |     types: | ||||||
|  |       - completed | ||||||
|  | permissions: | ||||||
|  |   contents: write | ||||||
|  |   packages: write | ||||||
|  |   pull-requests: write | ||||||
|  | env: | ||||||
|  |   DEFAULT_UV_VERSION: "0.8.x" | ||||||
|  |   DEFAULT_PYTHON_VERSION: "3.11" | ||||||
|  |   NLTK_DATA: "/usr/share/nltk_data" | ||||||
|  | jobs: | ||||||
|  |   prepare: | ||||||
|  |     if: >- | ||||||
|  |       github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' | ||||||
|  |     name: Prepare build context | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     outputs: | ||||||
|  |       should-build: ${{ steps.determine.outputs.should-build }} | ||||||
|  |       ref: ${{ steps.determine.outputs.ref }} | ||||||
|  |       ref-name: ${{ steps.determine.outputs.ref-name }} | ||||||
|  |       sha: ${{ steps.determine.outputs.sha }} | ||||||
|  |       is-tag: ${{ steps.determine.outputs.is-tag }} | ||||||
|  |       is-release-target: ${{ steps.determine.outputs.is-release-target }} | ||||||
|  |       is-beta-rc: ${{ steps.determine.outputs.is-beta-rc }} | ||||||
|  |     steps: | ||||||
|  |       - name: Determine ref information | ||||||
|  |         id: determine | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const run = context.payload.workflow_run; | ||||||
|  |             const owner = context.repo.owner; | ||||||
|  |             const repo = context.repo.repo; | ||||||
|  |             const sha = run.head_sha; | ||||||
|  |             const branch = run.head_branch; | ||||||
|  |  | ||||||
|  |             let ref = undefined; | ||||||
|  |             let refName = undefined; | ||||||
|  |  | ||||||
|  |             if (branch) { | ||||||
|  |               ref = `refs/heads/${branch}`; | ||||||
|  |               refName = branch; | ||||||
|  |             } else { | ||||||
|  |               const iterator = github.paginate.iterator( | ||||||
|  |                 github.rest.repos.listTags, | ||||||
|  |                 { | ||||||
|  |                   owner, | ||||||
|  |                   repo, | ||||||
|  |                   per_page: 100, | ||||||
|  |                 }, | ||||||
|  |               ); | ||||||
|  |  | ||||||
|  |               for await (const { data } of iterator) { | ||||||
|  |                 const match = data.find((tag) => tag.commit?.sha === sha); | ||||||
|  |                 if (match) { | ||||||
|  |                   ref = `refs/tags/${match.name}`; | ||||||
|  |                   refName = match.name; | ||||||
|  |                   break; | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const outputs = { | ||||||
|  |               shouldBuild: false, | ||||||
|  |               ref: ref ?? '', | ||||||
|  |               refName: refName ?? '', | ||||||
|  |               sha, | ||||||
|  |               isTag: ref?.startsWith('refs/tags/') ?? false, | ||||||
|  |               isReleaseTarget: false, | ||||||
|  |               isBetaRc: false, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if (!ref || !refName) { | ||||||
|  |               core.info('No matching ref found for workflow run; skipping post-CI workflow.'); | ||||||
|  |             } else { | ||||||
|  |               const allowed = | ||||||
|  |                 ref.startsWith('refs/heads/feature-') || | ||||||
|  |                 ref.startsWith('refs/heads/fix-') || | ||||||
|  |                 ref.startsWith('refs/heads/l10n_') || | ||||||
|  |                 ref === 'refs/heads/dev' || | ||||||
|  |                 ref === 'refs/heads/beta' || | ||||||
|  |                 ref.includes('beta.rc') || | ||||||
|  |                 ref.startsWith('refs/tags/v'); | ||||||
|  |  | ||||||
|  |               const isBetaRc = refName.includes('beta.rc'); | ||||||
|  |               const isReleaseTarget = outputs.isTag && (refName.startsWith('v') || isBetaRc); | ||||||
|  |  | ||||||
|  |               outputs.shouldBuild = allowed; | ||||||
|  |               outputs.isReleaseTarget = isReleaseTarget; | ||||||
|  |               outputs.isBetaRc = isBetaRc; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             core.setOutput('should-build', outputs.shouldBuild ? 'true' : 'false'); | ||||||
|  |             core.setOutput('ref', outputs.ref); | ||||||
|  |             core.setOutput('ref-name', outputs.refName); | ||||||
|  |             core.setOutput('sha', outputs.sha); | ||||||
|  |             core.setOutput('is-tag', outputs.isTag ? 'true' : 'false'); | ||||||
|  |             core.setOutput('is-release-target', outputs.isReleaseTarget ? 'true' : 'false'); | ||||||
|  |             core.setOutput('is-beta-rc', outputs.isBetaRc ? 'true' : 'false'); | ||||||
|  |   build-docker-image: | ||||||
|  |     needs: prepare | ||||||
|  |     if: needs.prepare.outputs.should-build == 'true' | ||||||
|  |     name: Build Docker image for ${{ needs.prepare.outputs.ref-name }} | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     concurrency: | ||||||
|  |       group: ${{ github.workflow }}-build-docker-image-${{ needs.prepare.outputs.ref-name || needs.prepare.outputs.sha }} | ||||||
|  |       cancel-in-progress: true | ||||||
|  |     env: | ||||||
|  |       REF: ${{ needs.prepare.outputs.ref }} | ||||||
|  |       REF_NAME: ${{ needs.prepare.outputs.ref-name }} | ||||||
|  |       SHA: ${{ needs.prepare.outputs.sha }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v5 | ||||||
|  |         with: | ||||||
|  |           ref: ${{ env.SHA }} | ||||||
|  |       - name: Check pushing to Docker Hub | ||||||
|  |         id: push-other-places | ||||||
|  |         env: | ||||||
|  |           REPOSITORY_OWNER: ${{ github.repository_owner }} | ||||||
|  |           REF_NAME: ${{ env.REF_NAME }} | ||||||
|  |           REF: ${{ env.REF }} | ||||||
|  |         run: | | ||||||
|  |           if [[ "$REPOSITORY_OWNER" == "paperless-ngx" ]] && \ | ||||||
|  |              ([[ "$REF_NAME" == "dev" ]] || [[ "$REF_NAME" == "beta" ]] || [[ "$REF" == refs/tags/v* ]]); then | ||||||
|  |             echo "Enabling DockerHub image push" | ||||||
|  |             echo "enable=true" >> "$GITHUB_OUTPUT" | ||||||
|  |           else | ||||||
|  |             echo "Not pushing to DockerHub" | ||||||
|  |             echo "enable=false" >> "$GITHUB_OUTPUT" | ||||||
|  |           fi | ||||||
|  |       - name: Set ghcr repository name | ||||||
|  |         id: set-ghcr-repository | ||||||
|  |         run: | | ||||||
|  |           ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }') | ||||||
|  |           echo "Name is ${ghcr_name}" | ||||||
|  |           echo "ghcr-repository=${ghcr_name}" >> "$GITHUB_OUTPUT" | ||||||
|  |       - name: Gather Docker metadata | ||||||
|  |         id: docker-meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         env: | ||||||
|  |           GITHUB_REF: ${{ env.REF }} | ||||||
|  |           GITHUB_REF_NAME: ${{ env.REF_NAME }} | ||||||
|  |           GITHUB_SHA: ${{ env.SHA }} | ||||||
|  |         with: | ||||||
|  |           images: | | ||||||
|  |             ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }} | ||||||
|  |             name=paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }} | ||||||
|  |             name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  |         with: | ||||||
|  |           platforms: arm64 | ||||||
|  |       - name: Login to GitHub Container Registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |       - name: Login to Docker Hub | ||||||
|  |         if: steps.push-other-places.outputs.enable == 'true' | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|  |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|  |       - name: Login to Quay.io | ||||||
|  |         if: steps.push-other-places.outputs.enable == 'true' | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: quay.io | ||||||
|  |           username: ${{ secrets.QUAY_USERNAME }} | ||||||
|  |           password: ${{ secrets.QUAY_ROBOT_TOKEN }} | ||||||
|  |       - name: Build and push | ||||||
|  |         uses: docker/build-push-action@v6 | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           file: ./Dockerfile | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|  |           push: true | ||||||
|  |           tags: ${{ steps.docker-meta.outputs.tags }} | ||||||
|  |           labels: ${{ steps.docker-meta.outputs.labels }} | ||||||
|  |           build-args: | | ||||||
|  |             PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} | ||||||
|  |           cache-from: | | ||||||
|  |             type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ env.REF_NAME }} | ||||||
|  |             type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev | ||||||
|  |           cache-to: | | ||||||
|  |             type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ env.REF_NAME }} | ||||||
|  |       - name: Inspect image | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||||
|  |       - name: Export frontend artifact from docker | ||||||
|  |         run: | | ||||||
|  |           docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} | ||||||
|  |           docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/ | ||||||
|  |       - name: Upload frontend artifact | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: frontend-compiled | ||||||
|  |           path: src/documents/static/frontend/ | ||||||
|  |           retention-days: 7 | ||||||
|  |   build-release: | ||||||
|  |     needs: | ||||||
|  |       - prepare | ||||||
|  |       - build-docker-image | ||||||
|  |     if: needs.prepare.outputs.should-build == 'true' | ||||||
|  |     name: Build release bundle | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     env: | ||||||
|  |       REF_NAME: ${{ needs.prepare.outputs.ref-name }} | ||||||
|  |       SHA: ${{ needs.prepare.outputs.sha }} | ||||||
|  |       CI_RUN_ID: ${{ github.event.workflow_run.id }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v5 | ||||||
|  |         with: | ||||||
|  |           ref: ${{ env.SHA }} | ||||||
|  |       - name: Set up Python | ||||||
|  |         id: setup-python | ||||||
|  |         uses: actions/setup-python@v5 | ||||||
|  |         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: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|  |       - name: Install Python dependencies | ||||||
|  |         run: | | ||||||
|  |           uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen | ||||||
|  |       - name: Install system dependencies | ||||||
|  |         run: | | ||||||
|  |           sudo apt-get update -qq | ||||||
|  |           sudo apt-get install -qq --no-install-recommends gettext liblept5 | ||||||
|  |       - name: Download frontend artifact | ||||||
|  |         uses: actions/download-artifact@v5 | ||||||
|  |         with: | ||||||
|  |           name: frontend-compiled | ||||||
|  |           path: src/documents/static/frontend/ | ||||||
|  |       - name: Download documentation artifact | ||||||
|  |         uses: actions/download-artifact@v5 | ||||||
|  |         with: | ||||||
|  |           name: documentation | ||||||
|  |           path: docs/_build/html/ | ||||||
|  |           run-id: ${{ env.CI_RUN_ID }} | ||||||
|  |       - name: Generate requirements file | ||||||
|  |         run: | | ||||||
|  |           uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt | ||||||
|  |       - name: Compile messages | ||||||
|  |         run: | | ||||||
|  |           cd src/ | ||||||
|  |           uv run \ | ||||||
|  |             --python ${{ steps.setup-python.outputs.python-version }} \ | ||||||
|  |             manage.py compilemessages | ||||||
|  |       - name: Collect static files | ||||||
|  |         run: | | ||||||
|  |           cd src/ | ||||||
|  |           uv run \ | ||||||
|  |             --python ${{ steps.setup-python.outputs.python-version }} \ | ||||||
|  |             manage.py collectstatic --no-input | ||||||
|  |       - name: Move files | ||||||
|  |         run: | | ||||||
|  |           echo "Making dist folders" | ||||||
|  |           for directory in dist \ | ||||||
|  |                           dist/paperless-ngx \ | ||||||
|  |                           dist/paperless-ngx/scripts; | ||||||
|  |           do | ||||||
|  |             mkdir --verbose --parents ${directory} | ||||||
|  |           done | ||||||
|  |  | ||||||
|  |           echo "Copying basic files" | ||||||
|  |           for file_name in .dockerignore \ | ||||||
|  |                           .env \ | ||||||
|  |                           Dockerfile \ | ||||||
|  |                           pyproject.toml \ | ||||||
|  |                           uv.lock \ | ||||||
|  |                           requirements.txt \ | ||||||
|  |                           LICENSE \ | ||||||
|  |                           README.md \ | ||||||
|  |                           paperless.conf.example | ||||||
|  |           do | ||||||
|  |             cp --verbose ${file_name} dist/paperless-ngx/ | ||||||
|  |           done | ||||||
|  |           mv --verbose dist/paperless-ngx/paperless.conf.example dist/paperless-ngx/paperless.conf | ||||||
|  |  | ||||||
|  |           echo "Copying Docker related files" | ||||||
|  |           cp --recursive docker/ dist/paperless-ngx/docker | ||||||
|  |  | ||||||
|  |           echo "Copying startup scripts" | ||||||
|  |           cp --verbose scripts/*.service scripts/*.sh scripts/*.socket dist/paperless-ngx/scripts/ | ||||||
|  |  | ||||||
|  |           echo "Copying source files" | ||||||
|  |           cp --recursive src/ dist/paperless-ngx/src | ||||||
|  |           echo "Copying documentation" | ||||||
|  |           cp --recursive docs/_build/html/ dist/paperless-ngx/docs | ||||||
|  |  | ||||||
|  |           mv --verbose static dist/paperless-ngx | ||||||
|  |       - name: Make release package | ||||||
|  |         run: | | ||||||
|  |           echo "Creating release archive" | ||||||
|  |           cd dist | ||||||
|  |           sudo chown -R 1000:1000 paperless-ngx/ | ||||||
|  |           tar -cJf paperless-ngx.tar.xz paperless-ngx/ | ||||||
|  |       - name: Upload release artifact | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: release | ||||||
|  |           path: dist/paperless-ngx.tar.xz | ||||||
|  |           retention-days: 7 | ||||||
|  |   publish-release: | ||||||
|  |     needs: | ||||||
|  |       - prepare | ||||||
|  |       - build-release | ||||||
|  |     if: needs.prepare.outputs.is-release-target == 'true' | ||||||
|  |     name: Publish release | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     outputs: | ||||||
|  |       prerelease: ${{ steps.get_version.outputs.prerelease }} | ||||||
|  |       changelog: ${{ steps.create-release.outputs.body }} | ||||||
|  |       version: ${{ steps.get_version.outputs.version }} | ||||||
|  |     steps: | ||||||
|  |       - name: Download release artifact | ||||||
|  |         uses: actions/download-artifact@v5 | ||||||
|  |         with: | ||||||
|  |           name: release | ||||||
|  |           path: ./ | ||||||
|  |       - name: Get version | ||||||
|  |         id: get_version | ||||||
|  |         run: | | ||||||
|  |           echo "version=${{ needs.prepare.outputs.ref-name }}" >> "$GITHUB_OUTPUT" | ||||||
|  |           if [[ ${{ needs.prepare.outputs.is-beta-rc }} == 'true' ]]; then | ||||||
|  |             echo "prerelease=true" >> "$GITHUB_OUTPUT" | ||||||
|  |           else | ||||||
|  |             echo "prerelease=false" >> "$GITHUB_OUTPUT" | ||||||
|  |           fi | ||||||
|  |       - name: Create Release and Changelog | ||||||
|  |         id: create-release | ||||||
|  |         uses: release-drafter/release-drafter@v6 | ||||||
|  |         with: | ||||||
|  |           name: Paperless-ngx ${{ steps.get_version.outputs.version }} | ||||||
|  |           tag: ${{ steps.get_version.outputs.version }} | ||||||
|  |           version: ${{ steps.get_version.outputs.version }} | ||||||
|  |           prerelease: ${{ steps.get_version.outputs.prerelease }} | ||||||
|  |           publish: true | ||||||
|  |         env: | ||||||
|  |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |       - name: Upload release archive | ||||||
|  |         id: upload-release-asset | ||||||
|  |         uses: shogo82148/actions-upload-release-asset@v1 | ||||||
|  |         with: | ||||||
|  |           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           upload_url: ${{ steps.create-release.outputs.upload_url }} | ||||||
|  |           asset_path: ./paperless-ngx.tar.xz | ||||||
|  |           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz | ||||||
|  |           asset_content_type: application/x-xz | ||||||
|  |   append-changelog: | ||||||
|  |     needs: | ||||||
|  |       - publish-release | ||||||
|  |     if: needs.publish-release.outputs.prerelease == 'false' | ||||||
|  |     name: Append changelog to docs | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v5 | ||||||
|  |         with: | ||||||
|  |           ref: main | ||||||
|  |       - name: Set up Python | ||||||
|  |         id: setup-python | ||||||
|  |         uses: actions/setup-python@v5 | ||||||
|  |         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: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|  |       - name: Append Changelog to docs | ||||||
|  |         id: append-Changelog | ||||||
|  |         working-directory: docs | ||||||
|  |         run: | | ||||||
|  |           git branch ${{ needs.publish-release.outputs.version }}-changelog | ||||||
|  |           git checkout ${{ needs.publish-release.outputs.version }}-changelog | ||||||
|  |           echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md | ||||||
|  |           echo "Manually linking usernames" | ||||||
|  |           sed -i -r 's|@([a-zA-Z0-9_]+) \(\[#|[@\1](https://github.com/\1) ([#|g' changelog-new.md | ||||||
|  |           echo "Removing unneeded comment tags" | ||||||
|  |           sed -i -r 's|@<!---->|@|g' changelog-new.md | ||||||
|  |           CURRENT_CHANGELOG=`tail --lines +2 changelog.md` | ||||||
|  |           echo -e "$CURRENT_CHANGELOG" >> changelog-new.md | ||||||
|  |           mv changelog-new.md changelog.md | ||||||
|  |           uv run \ | ||||||
|  |             --python ${{ steps.setup-python.outputs.python-version }} \ | ||||||
|  |             --dev \ | ||||||
|  |             pre-commit run --files changelog.md || true | ||||||
|  |           git config --global user.name "github-actions" | ||||||
|  |           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||||||
|  |           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" | ||||||
|  |           git push origin ${{ needs.publish-release.outputs.version }}-changelog | ||||||
|  |       - name: Create Pull Request | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const { repo, owner } = context.repo; | ||||||
|  |             const result = await github.rest.pulls.create({ | ||||||
|  |               title: 'Documentation: Add ${{ needs.publish-release.outputs.version }} changelog', | ||||||
|  |               owner, | ||||||
|  |               repo, | ||||||
|  |               head: '${{ needs.publish-release.outputs.version }}-changelog', | ||||||
|  |               base: 'main', | ||||||
|  |               body: 'This PR is auto-generated by CI.' | ||||||
|  |             }); | ||||||
|  |             github.rest.issues.addLabels({ | ||||||
|  |               owner, | ||||||
|  |               repo, | ||||||
|  |               issue_number: result.data.number, | ||||||
|  |               labels: ['documentation', 'skip-changelog'] | ||||||
|  |             }); | ||||||
							
								
								
									
										321
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										321
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -313,324 +313,3 @@ jobs: | |||||||
|         env: |         env: | ||||||
|           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} |           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||||||
|         run: cd src-ui && pnpm run build --configuration=production |         run: cd src-ui && pnpm run build --configuration=production | ||||||
|   build-docker-image: |  | ||||||
|     name: Build Docker image for ${{ github.ref_name }} |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/l10n_')) |  | ||||||
|     concurrency: |  | ||||||
|       group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }} |  | ||||||
|       cancel-in-progress: true |  | ||||||
|     needs: |  | ||||||
|       - tests-backend |  | ||||||
|       - tests-frontend |  | ||||||
|       - tests-frontend-e2e |  | ||||||
|     steps: |  | ||||||
|       - name: Check pushing to Docker Hub |  | ||||||
|         id: push-other-places |  | ||||||
|         # Only push to Dockerhub from the main repo AND the ref is either: |  | ||||||
|         #  main |  | ||||||
|         #  dev |  | ||||||
|         #  beta |  | ||||||
|         #  a tag |  | ||||||
|         # Otherwise forks would require a Docker Hub account and secrets setup |  | ||||||
|         run: | |  | ||||||
|           if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then |  | ||||||
|             echo "Enabling DockerHub image push" |  | ||||||
|             echo "enable=true" >> $GITHUB_OUTPUT |  | ||||||
|           else |  | ||||||
|             echo "Not pushing to DockerHub" |  | ||||||
|             echo "enable=false" >> $GITHUB_OUTPUT |  | ||||||
|           fi |  | ||||||
|       - name: Set ghcr repository name |  | ||||||
|         id: set-ghcr-repository |  | ||||||
|         run: | |  | ||||||
|           ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }') |  | ||||||
|           echo "Name is ${ghcr_name}" |  | ||||||
|           echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT |  | ||||||
|       - name: Gather Docker metadata |  | ||||||
|         id: docker-meta |  | ||||||
|         uses: docker/metadata-action@v5 |  | ||||||
|         with: |  | ||||||
|           images: | |  | ||||||
|             ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }} |  | ||||||
|             name=paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }} |  | ||||||
|             name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }} |  | ||||||
|           tags: | |  | ||||||
|             # Tag branches with branch name |  | ||||||
|             type=ref,event=branch |  | ||||||
|             # Process semver tags |  | ||||||
|             # For a tag x.y.z or vX.Y.Z, output an x.y.z and x.y image tag |  | ||||||
|             type=semver,pattern={{version}} |  | ||||||
|             type=semver,pattern={{major}}.{{minor}} |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v5 |  | ||||||
|       # If https://github.com/docker/buildx/issues/1044 is resolved, |  | ||||||
|       # the append input with a native arm64 arch could be used to |  | ||||||
|       # significantly speed up building |  | ||||||
|       - name: Set up Docker Buildx |  | ||||||
|         uses: docker/setup-buildx-action@v3 |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v3 |  | ||||||
|         with: |  | ||||||
|           platforms: arm64 |  | ||||||
|       - name: Login to GitHub Container Registry |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           registry: ghcr.io |  | ||||||
|           username: ${{ github.actor }} |  | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         # Don't attempt to login if not pushing to Docker Hub |  | ||||||
|         if: steps.push-other-places.outputs.enable == 'true' |  | ||||||
|         with: |  | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |  | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |  | ||||||
|       - name: Login to Quay.io |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         # Don't attempt to login if not pushing to Quay.io |  | ||||||
|         if: steps.push-other-places.outputs.enable == 'true' |  | ||||||
|         with: |  | ||||||
|           registry: quay.io |  | ||||||
|           username: ${{ secrets.QUAY_USERNAME }} |  | ||||||
|           password: ${{ secrets.QUAY_ROBOT_TOKEN }} |  | ||||||
|       - name: Build and push |  | ||||||
|         uses: docker/build-push-action@v6 |  | ||||||
|         with: |  | ||||||
|           context: . |  | ||||||
|           file: ./Dockerfile |  | ||||||
|           platforms: linux/amd64,linux/arm64 |  | ||||||
|           push: ${{ github.event_name != 'pull_request' }} |  | ||||||
|           tags: ${{ steps.docker-meta.outputs.tags }} |  | ||||||
|           labels: ${{ steps.docker-meta.outputs.labels }} |  | ||||||
|           build-args: | |  | ||||||
|             PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} |  | ||||||
|           # Get cache layers from this branch, then dev |  | ||||||
|           # This allows new branches to get at least some cache benefits, generally from dev |  | ||||||
|           cache-from: | |  | ||||||
|             type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }} |  | ||||||
|             type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev |  | ||||||
|           cache-to: | |  | ||||||
|             type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }} |  | ||||||
|       - name: Inspect image |  | ||||||
|         run: | |  | ||||||
|           docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} |  | ||||||
|       - name: Export frontend artifact from docker |  | ||||||
|         run: | |  | ||||||
|           docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} |  | ||||||
|           docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/ |  | ||||||
|       - name: Upload frontend artifact |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: frontend-compiled |  | ||||||
|           path: src/documents/static/frontend/ |  | ||||||
|           retention-days: 7 |  | ||||||
|   build-release: |  | ||||||
|     name: "Build Release" |  | ||||||
|     needs: |  | ||||||
|       - build-docker-image |  | ||||||
|       - documentation |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v5 |  | ||||||
|       - name: Set up Python |  | ||||||
|         id: setup-python |  | ||||||
|         uses: actions/setup-python@v5 |  | ||||||
|         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: Install Python dependencies |  | ||||||
|         run: | |  | ||||||
|           uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen |  | ||||||
|       - name: Install system dependencies |  | ||||||
|         run: | |  | ||||||
|           sudo apt-get update -qq |  | ||||||
|           sudo apt-get install -qq --no-install-recommends gettext liblept5 |  | ||||||
|       - name: Download frontend artifact |  | ||||||
|         uses: actions/download-artifact@v5 |  | ||||||
|         with: |  | ||||||
|           name: frontend-compiled |  | ||||||
|           path: src/documents/static/frontend/ |  | ||||||
|       - name: Download documentation artifact |  | ||||||
|         uses: actions/download-artifact@v5 |  | ||||||
|         with: |  | ||||||
|           name: documentation |  | ||||||
|           path: docs/_build/html/ |  | ||||||
|       - name: Generate requirements file |  | ||||||
|         run: | |  | ||||||
|           uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt |  | ||||||
|       - name: Compile messages |  | ||||||
|         run: | |  | ||||||
|           cd src/ |  | ||||||
|           uv run \ |  | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             manage.py compilemessages |  | ||||||
|       - name: Collect static files |  | ||||||
|         run: | |  | ||||||
|           cd src/ |  | ||||||
|           uv run \ |  | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             manage.py collectstatic --no-input |  | ||||||
|       - name: Move files |  | ||||||
|         run: | |  | ||||||
|           echo "Making dist folders" |  | ||||||
|           for directory in dist \ |  | ||||||
|                           dist/paperless-ngx \ |  | ||||||
|                           dist/paperless-ngx/scripts; |  | ||||||
|           do |  | ||||||
|             mkdir --verbose --parents ${directory} |  | ||||||
|           done |  | ||||||
|  |  | ||||||
|           echo "Copying basic files" |  | ||||||
|           for file_name in .dockerignore \ |  | ||||||
|                           .env \ |  | ||||||
|                           Dockerfile \ |  | ||||||
|                           pyproject.toml \ |  | ||||||
|                           uv.lock \ |  | ||||||
|                           requirements.txt \ |  | ||||||
|                           LICENSE \ |  | ||||||
|                           README.md \ |  | ||||||
|                           paperless.conf.example |  | ||||||
|           do |  | ||||||
|             cp --verbose ${file_name} dist/paperless-ngx/ |  | ||||||
|           done |  | ||||||
|           mv --verbose dist/paperless-ngx/paperless.conf.example dist/paperless-ngx/paperless.conf |  | ||||||
|  |  | ||||||
|           echo "Copying Docker related files" |  | ||||||
|           cp --recursive docker/ dist/paperless-ngx/docker |  | ||||||
|  |  | ||||||
|           echo "Copying startup scripts" |  | ||||||
|           cp --verbose scripts/*.service scripts/*.sh scripts/*.socket dist/paperless-ngx/scripts/ |  | ||||||
|  |  | ||||||
|           echo "Copying source files" |  | ||||||
|           cp --recursive src/ dist/paperless-ngx/src |  | ||||||
|           echo "Copying documentation" |  | ||||||
|           cp --recursive docs/_build/html/ dist/paperless-ngx/docs |  | ||||||
|  |  | ||||||
|           mv --verbose static dist/paperless-ngx |  | ||||||
|       - name: Make release package |  | ||||||
|         run: | |  | ||||||
|           echo "Creating release archive" |  | ||||||
|           cd dist |  | ||||||
|           sudo chown -R 1000:1000 paperless-ngx/ |  | ||||||
|           tar -cJf paperless-ngx.tar.xz paperless-ngx/ |  | ||||||
|       - name: Upload release artifact |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: release |  | ||||||
|           path: dist/paperless-ngx.tar.xz |  | ||||||
|           retention-days: 7 |  | ||||||
|   publish-release: |  | ||||||
|     name: "Publish Release" |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     outputs: |  | ||||||
|       prerelease: ${{ steps.get_version.outputs.prerelease }} |  | ||||||
|       changelog: ${{ steps.create-release.outputs.body }} |  | ||||||
|       version: ${{ steps.get_version.outputs.version }} |  | ||||||
|     needs: |  | ||||||
|       - build-release |  | ||||||
|     if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc')) |  | ||||||
|     steps: |  | ||||||
|       - name: Download release artifact |  | ||||||
|         uses: actions/download-artifact@v5 |  | ||||||
|         with: |  | ||||||
|           name: release |  | ||||||
|           path: ./ |  | ||||||
|       - name: Get version |  | ||||||
|         id: get_version |  | ||||||
|         run: | |  | ||||||
|           echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT |  | ||||||
|           if [[ ${{ contains(github.ref_name, '-beta.rc') }} == 'true' ]]; then |  | ||||||
|             echo "prerelease=true" >> $GITHUB_OUTPUT |  | ||||||
|           else |  | ||||||
|             echo "prerelease=false" >> $GITHUB_OUTPUT |  | ||||||
|           fi |  | ||||||
|       - name: Create Release and Changelog |  | ||||||
|         id: create-release |  | ||||||
|         uses: release-drafter/release-drafter@v6 |  | ||||||
|         with: |  | ||||||
|           name: Paperless-ngx ${{ steps.get_version.outputs.version }} |  | ||||||
|           tag: ${{ steps.get_version.outputs.version }} |  | ||||||
|           version: ${{ steps.get_version.outputs.version }} |  | ||||||
|           prerelease: ${{ steps.get_version.outputs.prerelease }} |  | ||||||
|           publish: true # ensures release is not marked as draft |  | ||||||
|         env: |  | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|       - name: Upload release archive |  | ||||||
|         id: upload-release-asset |  | ||||||
|         uses: shogo82148/actions-upload-release-asset@v1 |  | ||||||
|         with: |  | ||||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|           upload_url: ${{ steps.create-release.outputs.upload_url }} |  | ||||||
|           asset_path: ./paperless-ngx.tar.xz |  | ||||||
|           asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz |  | ||||||
|           asset_content_type: application/x-xz |  | ||||||
|   append-changelog: |  | ||||||
|     name: "Append Changelog" |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - publish-release |  | ||||||
|     if: needs.publish-release.outputs.prerelease == 'false' |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v5 |  | ||||||
|         with: |  | ||||||
|           ref: main |  | ||||||
|       - name: Set up Python |  | ||||||
|         id: setup-python |  | ||||||
|         uses: actions/setup-python@v5 |  | ||||||
|         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: ${{ env.DEFAULT_PYTHON_VERSION }} |  | ||||||
|       - name: Append Changelog to docs |  | ||||||
|         id: append-Changelog |  | ||||||
|         working-directory: docs |  | ||||||
|         run: | |  | ||||||
|           git branch ${{ needs.publish-release.outputs.version }}-changelog |  | ||||||
|           git checkout ${{ needs.publish-release.outputs.version }}-changelog |  | ||||||
|           echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md |  | ||||||
|           echo "Manually linking usernames" |  | ||||||
|           sed -i -r 's|@([a-zA-Z0-9_]+) \(\[#|[@\1](https://github.com/\1) ([#|g' changelog-new.md |  | ||||||
|           echo "Removing unneeded comment tags" |  | ||||||
|           sed -i -r 's|@<!---->|@|g' changelog-new.md |  | ||||||
|           CURRENT_CHANGELOG=`tail --lines +2 changelog.md` |  | ||||||
|           echo -e "$CURRENT_CHANGELOG" >> changelog-new.md |  | ||||||
|           mv changelog-new.md changelog.md |  | ||||||
|           uv run \ |  | ||||||
|             --python ${{ steps.setup-python.outputs.python-version }} \ |  | ||||||
|             --dev \ |  | ||||||
|             pre-commit run --files changelog.md || true |  | ||||||
|           git config --global user.name "github-actions" |  | ||||||
|           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" |  | ||||||
|           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" |  | ||||||
|           git push origin ${{ needs.publish-release.outputs.version }}-changelog |  | ||||||
|       - name: Create Pull Request |  | ||||||
|         uses: actions/github-script@v7 |  | ||||||
|         with: |  | ||||||
|           script: | |  | ||||||
|             const { repo, owner } = context.repo; |  | ||||||
|             const result = await github.rest.pulls.create({ |  | ||||||
|               title: 'Documentation: Add ${{ needs.publish-release.outputs.version }} changelog', |  | ||||||
|               owner, |  | ||||||
|               repo, |  | ||||||
|               head: '${{ needs.publish-release.outputs.version }}-changelog', |  | ||||||
|               base: 'main', |  | ||||||
|               body: 'This PR is auto-generated by CI.' |  | ||||||
|             }); |  | ||||||
|             github.rest.issues.addLabels({ |  | ||||||
|               owner, |  | ||||||
|               repo, |  | ||||||
|               issue_number: result.data.number, |  | ||||||
|               labels: ['documentation', 'skip-changelog'] |  | ||||||
|             }); |  | ||||||
|   | |||||||
							
								
								
									
										220
									
								
								.github/workflows/codecov-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								.github/workflows/codecov-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,220 @@ | |||||||
|  | name: Codecov PR Comment | ||||||
|  | on: | ||||||
|  |   workflow_run: | ||||||
|  |     workflows: | ||||||
|  |       - ci | ||||||
|  |     types: | ||||||
|  |       - completed | ||||||
|  | permissions: | ||||||
|  |   contents: read | ||||||
|  |   pull-requests: write | ||||||
|  | jobs: | ||||||
|  |   comment: | ||||||
|  |     if: >- | ||||||
|  |       github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' | ||||||
|  |     runs-on: ubuntu-24.04 | ||||||
|  |     steps: | ||||||
|  |       - name: Gather pull request context | ||||||
|  |         id: pr | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const run = context.payload.workflow_run; | ||||||
|  |             if (!run.pull_requests || run.pull_requests.length === 0) { | ||||||
|  |               core.info('No associated pull request. Skipping.'); | ||||||
|  |               return { shouldRun: false }; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const pr = run.pull_requests[0]; | ||||||
|  |             return { | ||||||
|  |               shouldRun: true, | ||||||
|  |               prNumber: pr.number, | ||||||
|  |               headSha: run.head_sha, | ||||||
|  |             }; | ||||||
|  |       - name: Fetch Codecov coverage | ||||||
|  |         id: coverage | ||||||
|  |         if: steps.pr.outputs.shouldRun == 'true' | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         env: | ||||||
|  |           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||||||
|  |           COMMIT_SHA: ${{ steps.pr.outputs.headSha }} | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const token = process.env.CODECOV_TOKEN; | ||||||
|  |             if (!token) { | ||||||
|  |               core.warning('CODECOV_TOKEN secret is not available; skipping comment.'); | ||||||
|  |               core.setOutput('shouldComment', 'false'); | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const commitSha = process.env.COMMIT_SHA; | ||||||
|  |             const owner = context.repo.owner; | ||||||
|  |             const repo = context.repo.repo; | ||||||
|  |             const url = `https://codecov.io/api/v2/github/${owner}/repos/${repo}/commits/${commitSha}/report`; | ||||||
|  |             const maxAttempts = 10; | ||||||
|  |             const waitMs = 15000; | ||||||
|  |             const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | ||||||
|  |  | ||||||
|  |             let data; | ||||||
|  |             for (let attempt = 1; attempt <= maxAttempts; attempt++) { | ||||||
|  |               core.info(`Fetching Codecov report (attempt ${attempt}/${maxAttempts})`); | ||||||
|  |               const response = await fetch(url, { | ||||||
|  |                 headers: { | ||||||
|  |                   Authorization: `Bearer ${token}`, | ||||||
|  |                   'Content-Type': 'application/json', | ||||||
|  |                   Accept: 'application/json', | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |  | ||||||
|  |               if (response.status === 404) { | ||||||
|  |                 core.info('Report not ready yet (404). Waiting before retrying.'); | ||||||
|  |                 await sleep(waitMs); | ||||||
|  |                 continue; | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               if (!response.ok) { | ||||||
|  |                 const text = await response.text(); | ||||||
|  |                 throw new Error(`Codecov API returned ${response.status}: ${text}`); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               data = await response.json(); | ||||||
|  |               if (data && Object.keys(data).length > 0) { | ||||||
|  |                 break; | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               core.info('Report payload empty. Waiting before retrying.'); | ||||||
|  |               await sleep(waitMs); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (!data) { | ||||||
|  |               core.warning('Unable to retrieve Codecov report after multiple attempts.'); | ||||||
|  |               core.setOutput('shouldComment', 'false'); | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const totals = data.report?.totals ?? data.commit?.totals ?? data.totals; | ||||||
|  |             if (!totals) { | ||||||
|  |               core.warning('Codecov response does not contain coverage totals.'); | ||||||
|  |               core.setOutput('shouldComment', 'false'); | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const compareTotals = data.report?.compare?.totals ?? data.compare?.totals; | ||||||
|  |             const flagsRaw = data.report?.totals_by_flag ?? data.report?.components ?? []; | ||||||
|  |  | ||||||
|  |             const toNumber = (value) => { | ||||||
|  |               if (value === null || value === undefined || value === '') { | ||||||
|  |                 return undefined; | ||||||
|  |               } | ||||||
|  |               const num = Number(value); | ||||||
|  |               return Number.isFinite(num) ? num : undefined; | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             const coverage = toNumber(totals.coverage); | ||||||
|  |             const baseCoverage = toNumber(compareTotals?.base_coverage ?? compareTotals?.base); | ||||||
|  |             const delta = toNumber( | ||||||
|  |               compareTotals?.coverage_change ?? | ||||||
|  |               compareTotals?.coverage_diff ?? | ||||||
|  |               totals.delta ?? | ||||||
|  |               totals.diff ?? | ||||||
|  |               totals.change, | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             const formatPercent = (value) => { | ||||||
|  |               if (value === undefined) return '—'; | ||||||
|  |               return `${value.toFixed(2)}%`; | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             const formatDelta = (value) => { | ||||||
|  |               if (value === undefined) return '—'; | ||||||
|  |               const sign = value >= 0 ? '+' : ''; | ||||||
|  |               return `${sign}${value.toFixed(2)}%`; | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             const shortSha = commitSha.slice(0, 7); | ||||||
|  |             const lines = [ | ||||||
|  |               '<!-- codecov-coverage-comment -->', | ||||||
|  |               '**Codecov Coverage**', | ||||||
|  |               '', | ||||||
|  |               `- Head \`${shortSha}\`: ${formatPercent(coverage)}`, | ||||||
|  |             ]; | ||||||
|  |  | ||||||
|  |             if (baseCoverage !== undefined) { | ||||||
|  |               lines.push(`- Base: ${formatPercent(baseCoverage)}`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (delta !== undefined) { | ||||||
|  |               lines.push(`- Change: ${formatDelta(delta)}`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const flagEntries = Array.isArray(flagsRaw) | ||||||
|  |               ? flagsRaw | ||||||
|  |               : Object.entries(flagsRaw).map(([name, totals]) => ({ name, totals })); | ||||||
|  |  | ||||||
|  |             const flagRows = []; | ||||||
|  |             for (const entry of flagEntries) { | ||||||
|  |               const label = entry.flag ?? entry.name ?? entry.component ?? entry.id; | ||||||
|  |               const entryTotals = entry.totals ?? entry; | ||||||
|  |               const entryCoverage = toNumber(entryTotals?.coverage); | ||||||
|  |               const entryDelta = toNumber( | ||||||
|  |                 entryTotals?.coverage_change ?? | ||||||
|  |                 entryTotals?.coverage_diff ?? | ||||||
|  |                 entryTotals?.delta ?? | ||||||
|  |                 entryTotals?.diff, | ||||||
|  |               ); | ||||||
|  |               if (!label || entryCoverage === undefined) { | ||||||
|  |                 continue; | ||||||
|  |               } | ||||||
|  |               flagRows.push(`| ${label} | ${formatPercent(entryCoverage)} | ${formatDelta(entryDelta)} |`); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (flagRows.length) { | ||||||
|  |               lines.push(''); | ||||||
|  |               lines.push('| Flag | Coverage | Change |'); | ||||||
|  |               lines.push('| --- | --- | --- |'); | ||||||
|  |               lines.push(...flagRows); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const commentBody = lines.join('\n'); | ||||||
|  |             const shouldComment = coverage !== undefined; | ||||||
|  |             core.setOutput('shouldComment', shouldComment ? 'true' : 'false'); | ||||||
|  |             if (shouldComment) { | ||||||
|  |               core.setOutput('commentBody', commentBody); | ||||||
|  |             } | ||||||
|  |       - name: Upsert coverage comment | ||||||
|  |         if: steps.pr.outputs.shouldRun == 'true' && steps.coverage.outputs.shouldComment == 'true' | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         env: | ||||||
|  |           PR_NUMBER: ${{ steps.pr.outputs.prNumber }} | ||||||
|  |           COMMENT_BODY: ${{ steps.coverage.outputs.commentBody }} | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const prNumber = Number(process.env.PR_NUMBER); | ||||||
|  |             const body = process.env.COMMENT_BODY; | ||||||
|  |             const marker = '<!-- codecov-coverage-comment -->'; | ||||||
|  |  | ||||||
|  |             const { data: comments } = await github.rest.issues.listComments({ | ||||||
|  |               owner: context.repo.owner, | ||||||
|  |               repo: context.repo.repo, | ||||||
|  |               issue_number: prNumber, | ||||||
|  |               per_page: 100, | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             const existing = comments.find((comment) => comment.body?.includes(marker)); | ||||||
|  |             if (existing) { | ||||||
|  |               core.info(`Updating existing coverage comment (id: ${existing.id}).`); | ||||||
|  |               await github.rest.issues.updateComment({ | ||||||
|  |                 owner: context.repo.owner, | ||||||
|  |                 repo: context.repo.repo, | ||||||
|  |                 comment_id: existing.id, | ||||||
|  |                 body, | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               core.info('Creating new coverage comment.'); | ||||||
|  |               await github.rest.issues.createComment({ | ||||||
|  |                 owner: context.repo.owner, | ||||||
|  |                 repo: context.repo.repo, | ||||||
|  |                 issue_number: prNumber, | ||||||
|  |                 body, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
							
								
								
									
										1
									
								
								.github/workflows/repo-maintenance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/repo-maintenance.yml
									
									
									
									
										vendored
									
									
								
							| @@ -241,6 +241,7 @@ jobs: | |||||||
|                 ) { |                 ) { | ||||||
|                   nodes { |                   nodes { | ||||||
|                     id, |                     id, | ||||||
|  |                     createdAt, | ||||||
|                     number, |                     number, | ||||||
|                     updatedAt, |                     updatedAt, | ||||||
|                     upvoteCount, |                     upvoteCount, | ||||||
|   | |||||||
| @@ -2,9 +2,11 @@ | |||||||
|  |  | ||||||
| If you feel like contributing to the project, please do! Bug fixes and improvements are always welcome. | If you feel like contributing to the project, please do! Bug fixes and improvements are always welcome. | ||||||
|  |  | ||||||
|  | ⚠️ Please note: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Pull requests that are opened without meeting this requirement may not be merged. | ||||||
|  |  | ||||||
| If you want to implement something big: | If you want to implement something big: | ||||||
|  |  | ||||||
| - Please start a discussion about that in the issues! Maybe something similar is already in development and we can make it happen together. | - As above, please start with a discussion! Maybe something similar is already in development and we can make it happen together. | ||||||
| - When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project. | - When making additions to the project, consider if the majority of users will benefit from your change. If not, you're probably better of forking the project. | ||||||
| - Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change. | - Also consider if your change will get in the way of other users. A good change is a change that enhances the experience of some users who want that change and does not affect users who do not care about the change. | ||||||
| - Please see the [paperless-ngx merge process](#merging-prs) below. | - Please see the [paperless-ngx merge process](#merging-prs) below. | ||||||
| @@ -133,7 +135,7 @@ community members. That said, in an effort to keep the repository organized and | |||||||
| - Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity. | - Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity. | ||||||
| - Discussions with a marked answer will be automatically closed. | - Discussions with a marked answer will be automatically closed. | ||||||
| - Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity. | - Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity. | ||||||
| - Feature requests that do not meet the following thresholds will be closed: 180 days of inactivity, < 5 "up-votes" after 180 days, < 20 "up-votes" after 1 year or < 80 "up-votes" at 2 years. | - Feature requests that do not meet the following thresholds will be closed: 180 days of inactivity with less than 80 "up-votes", < 5 "up-votes" after 180 days, < 20 "up-votes" after 1 year or < 40 "up-votes" at 2 years. | ||||||
|  |  | ||||||
| In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns. | In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns. | ||||||
| Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features. | Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features. | ||||||
|   | |||||||
| @@ -1759,6 +1759,11 @@ started by the container. | |||||||
|  |  | ||||||
| : Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg` | : Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg` | ||||||
|  |  | ||||||
|  | !!! note | ||||||
|  |  | ||||||
|  |     The logo file will be viewable by anyone with access to the Paperless instance login page, | ||||||
|  |     so consider your choice of logo carefully and removing exif data from images before uploading. | ||||||
|  |  | ||||||
| #### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK} | #### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK} | ||||||
|  |  | ||||||
| !!! note | !!! note | ||||||
|   | |||||||
| @@ -755,11 +755,15 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">122</context> |           <context context-type="linenumber">123</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">186</context> |           <context context-type="linenumber">192</context> | ||||||
|  |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">16</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||||
| @@ -972,6 +976,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> |           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> | ||||||
|           <context context-type="linenumber">4</context> |           <context context-type="linenumber">4</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">3</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6226301160429720843" datatype="html"> |       <trans-unit id="6226301160429720843" datatype="html"> | ||||||
|         <source> Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. </source> |         <source> Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. </source> | ||||||
| @@ -1217,11 +1225,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">148</context> |           <context context-type="linenumber">154</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">160</context> |           <context context-type="linenumber">166</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||||
| @@ -1812,7 +1820,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">115</context> |           <context context-type="linenumber">116</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||||
| @@ -2004,6 +2012,14 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> | ||||||
|           <context context-type="linenumber">14</context> |           <context context-type="linenumber">14</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">87</context> | ||||||
|  |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">89</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8597030111956627342" datatype="html"> |       <trans-unit id="8597030111956627342" datatype="html"> | ||||||
|         <source>Empty trash</source> |         <source>Empty trash</source> | ||||||
| @@ -2113,11 +2129,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">149</context> |           <context context-type="linenumber">155</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">163</context> |           <context context-type="linenumber">169</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||||
| @@ -2241,11 +2257,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">191</context> |           <context context-type="linenumber">192</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">292</context> |           <context context-type="linenumber">293</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> | ||||||
| @@ -2432,11 +2448,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">147</context> |           <context context-type="linenumber">153</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">157</context> |           <context context-type="linenumber">163</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> | ||||||
| @@ -2568,11 +2584,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">193</context> |           <context context-type="linenumber">194</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">294</context> |           <context context-type="linenumber">295</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> | ||||||
| @@ -3129,6 +3145,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/clearable-badge/clearable-badge.component.html</context> |           <context context-type="sourcefile">src/app/components/common/clearable-badge/clearable-badge.component.html</context> | ||||||
|           <context context-type="linenumber">2</context> |           <context context-type="linenumber">2</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">85</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7515883357904500238" datatype="html"> |       <trans-unit id="7515883357904500238" datatype="html"> | ||||||
|         <source>Are you sure?</source> |         <source>Are you sure?</source> | ||||||
| @@ -3896,7 +3916,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">136</context> |           <context context-type="linenumber">137</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||||
| @@ -4106,6 +4126,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context> |           <context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context> | ||||||
|           <context context-type="linenumber">30</context> |           <context context-type="linenumber">30</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">36</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6886003843406464884" datatype="html"> |       <trans-unit id="6886003843406464884" datatype="html"> | ||||||
|         <source>Only process attachments</source> |         <source>Only process attachments</source> | ||||||
| @@ -5109,6 +5133,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">11</context> |           <context context-type="linenumber">11</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">32</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8066608938393600549" datatype="html"> |       <trans-unit id="8066608938393600549" datatype="html"> | ||||||
|         <source>Message</source> |         <source>Message</source> | ||||||
| @@ -5478,6 +5506,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> |           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> | ||||||
|           <context context-type="linenumber">9</context> |           <context context-type="linenumber">9</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">7</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5034217198277582100" datatype="html"> |       <trans-unit id="5034217198277582100" datatype="html"> | ||||||
|         <source>Select all pages</source> |         <source>Select all pages</source> | ||||||
| @@ -5745,11 +5777,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">150</context> |           <context context-type="linenumber">156</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">168</context> |           <context context-type="linenumber">174</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||||
| @@ -6127,6 +6159,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">114</context> |           <context context-type="linenumber">114</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">35</context> | ||||||
|  |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||||
|           <context context-type="linenumber">19</context> |           <context context-type="linenumber">19</context> | ||||||
| @@ -8517,185 +8553,227 @@ | |||||||
|         <source>Disabled</source> |         <source>Disabled</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">136</context> |           <context context-type="linenumber">137</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||||
|           <context context-type="linenumber">41</context> |           <context context-type="linenumber">41</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="8996068874121140407" datatype="html"> | ||||||
|  |         <source>View Processed Mail</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|  |           <context context-type="linenumber">143</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="6751234988479444294" datatype="html"> |       <trans-unit id="6751234988479444294" datatype="html"> | ||||||
|         <source>No mail rules defined.</source> |         <source>No mail rules defined.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> | ||||||
|           <context context-type="linenumber">177</context> |           <context context-type="linenumber">183</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3178554336792037159" datatype="html"> |       <trans-unit id="3178554336792037159" datatype="html"> | ||||||
|         <source>Error retrieving mail accounts</source> |         <source>Error retrieving mail accounts</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">104</context> |           <context context-type="linenumber">105</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5241231471117657636" datatype="html"> |       <trans-unit id="5241231471117657636" datatype="html"> | ||||||
|         <source>Error retrieving mail rules</source> |         <source>Error retrieving mail rules</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">126</context> |           <context context-type="linenumber">127</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="763945516325093575" datatype="html"> |       <trans-unit id="763945516325093575" datatype="html"> | ||||||
|         <source>OAuth2 authentication success</source> |         <source>OAuth2 authentication success</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">134</context> |           <context context-type="linenumber">135</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9022978370268070156" datatype="html"> |       <trans-unit id="9022978370268070156" datatype="html"> | ||||||
|         <source>OAuth2 authentication failed, see logs for details</source> |         <source>OAuth2 authentication failed, see logs for details</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">145</context> |           <context context-type="linenumber">146</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6327501535846658797" datatype="html"> |       <trans-unit id="6327501535846658797" datatype="html"> | ||||||
|         <source>Saved account "<x id="PH" equiv-text="newMailAccount.name"/>".</source> |         <source>Saved account "<x id="PH" equiv-text="newMailAccount.name"/>".</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">169</context> |           <context context-type="linenumber">170</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8067594003836508139" datatype="html"> |       <trans-unit id="8067594003836508139" datatype="html"> | ||||||
|         <source>Error saving account.</source> |         <source>Error saving account.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">181</context> |           <context context-type="linenumber">182</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5641934153807844674" datatype="html"> |       <trans-unit id="5641934153807844674" datatype="html"> | ||||||
|         <source>Confirm delete mail account</source> |         <source>Confirm delete mail account</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">189</context> |           <context context-type="linenumber">190</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7176985344323395435" datatype="html"> |       <trans-unit id="7176985344323395435" datatype="html"> | ||||||
|         <source>This operation will permanently delete this mail account.</source> |         <source>This operation will permanently delete this mail account.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">190</context> |           <context context-type="linenumber">191</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5876433590301754883" datatype="html"> |       <trans-unit id="5876433590301754883" datatype="html"> | ||||||
|         <source>Deleted mail account "<x id="PH" equiv-text="account.name"/>"</source> |         <source>Deleted mail account "<x id="PH" equiv-text="account.name"/>"</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">200</context> |           <context context-type="linenumber">201</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5981429299543258715" datatype="html"> |       <trans-unit id="5981429299543258715" datatype="html"> | ||||||
|         <source>Error deleting mail account "<x id="PH" equiv-text="account.name"/>".</source> |         <source>Error deleting mail account "<x id="PH" equiv-text="account.name"/>".</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">211</context> |           <context context-type="linenumber">212</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6424800796582120505" datatype="html"> |       <trans-unit id="6424800796582120505" datatype="html"> | ||||||
|         <source>Processing mail account "<x id="PH" equiv-text="account.name"/>"</source> |         <source>Processing mail account "<x id="PH" equiv-text="account.name"/>"</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">223</context> |           <context context-type="linenumber">224</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3138185874003827652" datatype="html"> |       <trans-unit id="3138185874003827652" datatype="html"> | ||||||
|         <source>Error processing mail account "<x id="PH" equiv-text="account.name"/>"</source> |         <source>Error processing mail account "<x id="PH" equiv-text="account.name"/>"</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">228</context> |           <context context-type="linenumber">229</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="123368655395433699" datatype="html"> |       <trans-unit id="123368655395433699" datatype="html"> | ||||||
|         <source>Saved rule "<x id="PH" equiv-text="newMailRule.name"/>".</source> |         <source>Saved rule "<x id="PH" equiv-text="newMailRule.name"/>".</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">246</context> |           <context context-type="linenumber">247</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8951124554918814321" datatype="html"> |       <trans-unit id="8951124554918814321" datatype="html"> | ||||||
|         <source>Error saving rule.</source> |         <source>Error saving rule.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">257</context> |           <context context-type="linenumber">258</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3574401690710711341" datatype="html"> |       <trans-unit id="3574401690710711341" datatype="html"> | ||||||
|         <source>Rule "<x id="PH" equiv-text="rule.name"/>" enabled.</source> |         <source>Rule "<x id="PH" equiv-text="rule.name"/>" enabled.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">273</context> |           <context context-type="linenumber">274</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7171685227222299542" datatype="html"> |       <trans-unit id="7171685227222299542" datatype="html"> | ||||||
|         <source>Rule "<x id="PH" equiv-text="rule.name"/>" disabled.</source> |         <source>Rule "<x id="PH" equiv-text="rule.name"/>" disabled.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">274</context> |           <context context-type="linenumber">275</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7238791203524413596" datatype="html"> |       <trans-unit id="7238791203524413596" datatype="html"> | ||||||
|         <source>Error toggling rule "<x id="PH" equiv-text="rule.name"/>".</source> |         <source>Error toggling rule "<x id="PH" equiv-text="rule.name"/>".</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">279</context> |           <context context-type="linenumber">280</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3896080636020672118" datatype="html"> |       <trans-unit id="3896080636020672118" datatype="html"> | ||||||
|         <source>Confirm delete mail rule</source> |         <source>Confirm delete mail rule</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">290</context> |           <context context-type="linenumber">291</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2250372580580310337" datatype="html"> |       <trans-unit id="2250372580580310337" datatype="html"> | ||||||
|         <source>This operation will permanently delete this mail rule.</source> |         <source>This operation will permanently delete this mail rule.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">291</context> |           <context context-type="linenumber">292</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4357654589451732716" datatype="html"> |       <trans-unit id="4357654589451732716" datatype="html"> | ||||||
|         <source>Deleted mail rule "<x id="PH" equiv-text="rule.name"/>"</source> |         <source>Deleted mail rule "<x id="PH" equiv-text="rule.name"/>"</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">301</context> |           <context context-type="linenumber">302</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1696130068388341598" datatype="html"> |       <trans-unit id="1696130068388341598" datatype="html"> | ||||||
|         <source>Error deleting mail rule "<x id="PH" equiv-text="rule.name"/>".</source> |         <source>Error deleting mail rule "<x id="PH" equiv-text="rule.name"/>".</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">312</context> |           <context context-type="linenumber">313</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3061362835271417984" datatype="html"> |       <trans-unit id="3061362835271417984" datatype="html"> | ||||||
|         <source>Permissions updated</source> |         <source>Permissions updated</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">336</context> |           <context context-type="linenumber">337</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4639647950943944112" datatype="html"> |       <trans-unit id="4639647950943944112" datatype="html"> | ||||||
|         <source>Error updating permissions</source> |         <source>Error updating permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context> | ||||||
|           <context context-type="linenumber">341</context> |           <context context-type="linenumber">342</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> | ||||||
|           <context context-type="linenumber">339</context> |           <context context-type="linenumber">339</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="3501895737484542570" datatype="html"> | ||||||
|  |         <source>Processed Mail for <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="<em>"/><x id="INTERPOLATION" equiv-text="{{ rule.name }}"/><x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/></source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">2</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="1991019495862291373" datatype="html"> | ||||||
|  |         <source>No processed email messages found.</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">20</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="8691920320483720007" datatype="html"> | ||||||
|  |         <source>Received</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">33</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="4749295647449765550" datatype="html"> | ||||||
|  |         <source>Processed</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">34</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="2175109571923803648" datatype="html"> | ||||||
|  |         <source>Processed mail(s) deleted</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.ts</context> | ||||||
|  |           <context context-type="linenumber">72</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="4010735610815226758" datatype="html"> |       <trans-unit id="4010735610815226758" datatype="html"> | ||||||
|         <source>Filter by:</source> |         <source>Filter by:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|   | |||||||
| @@ -71,4 +71,20 @@ describe('TagListComponent', () => { | |||||||
|       'Do you really want to delete the tag "Tag1"?' |       'Do you really want to delete the tag "Tag1"?' | ||||||
|     ) |     ) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   it('should filter out child tags if name filter is empty, otherwise show all', () => { | ||||||
|  |     const tags = [ | ||||||
|  |       { id: 1, name: 'Tag1', parent: null }, | ||||||
|  |       { id: 2, name: 'Tag2', parent: 1 }, | ||||||
|  |       { id: 3, name: 'Tag3', parent: null }, | ||||||
|  |     ] | ||||||
|  |     component['_nameFilter'] = null // Simulate empty name filter | ||||||
|  |     const filtered = component.filterData(tags as any) | ||||||
|  |     expect(filtered.length).toBe(2) | ||||||
|  |     expect(filtered.find((t) => t.id === 2)).toBeUndefined() | ||||||
|  |  | ||||||
|  |     component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter | ||||||
|  |     const filteredWithName = component.filterData(tags as any) | ||||||
|  |     expect(filteredWithName.length).toBe(3) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -62,6 +62,8 @@ export class TagListComponent extends ManagementListComponent<Tag> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   filterData(data: Tag[]) { |   filterData(data: Tag[]) { | ||||||
|     return data.filter((tag) => !tag.parent) |     return this.nameFilter?.length | ||||||
|  |       ? [...data] | ||||||
|  |       : data.filter((tag) => !tag.parent) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -164,6 +164,9 @@ class BarcodePlugin(ConsumeTaskPlugin): | |||||||
|                         mailrule_id=self.input_doc.mailrule_id, |                         mailrule_id=self.input_doc.mailrule_id, | ||||||
|                         # Can't use same folder or the consume might grab it again |                         # Can't use same folder or the consume might grab it again | ||||||
|                         original_file=(tmp_dir / new_document.name).resolve(), |                         original_file=(tmp_dir / new_document.name).resolve(), | ||||||
|  |                         # Adding optional original_path for later uses in | ||||||
|  |                         # workflow matching | ||||||
|  |                         original_path=self.input_doc.original_file, | ||||||
|                     ), |                     ), | ||||||
|                     # All the same metadata |                     # All the same metadata | ||||||
|                     self.metadata, |                     self.metadata, | ||||||
|   | |||||||
| @@ -156,6 +156,7 @@ class ConsumableDocument: | |||||||
|  |  | ||||||
|     source: DocumentSource |     source: DocumentSource | ||||||
|     original_file: Path |     original_file: Path | ||||||
|  |     original_path: Path | None = None | ||||||
|     mailrule_id: int | None = None |     mailrule_id: int | None = None | ||||||
|     mime_type: str = dataclasses.field(init=False, default=None) |     mime_type: str = dataclasses.field(init=False, default=None) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -82,6 +82,13 @@ def _is_ignored(filepath: Path) -> bool: | |||||||
|  |  | ||||||
|  |  | ||||||
| def _consume(filepath: Path) -> None: | def _consume(filepath: Path) -> None: | ||||||
|  |     # Check permissions early | ||||||
|  |     try: | ||||||
|  |         filepath.stat() | ||||||
|  |     except (PermissionError, OSError): | ||||||
|  |         logger.warning(f"Not consuming file {filepath}: Permission denied.") | ||||||
|  |         return | ||||||
|  |  | ||||||
|     if filepath.is_dir() or _is_ignored(filepath): |     if filepath.is_dir() or _is_ignored(filepath): | ||||||
|         return |         return | ||||||
|  |  | ||||||
| @@ -323,7 +330,12 @@ class Command(BaseCommand): | |||||||
|  |  | ||||||
|                         # Also make sure the file exists still, some scanners might write a |                         # Also make sure the file exists still, some scanners might write a | ||||||
|                         # temporary file first |                         # temporary file first | ||||||
|                         file_still_exists = filepath.exists() and filepath.is_file() |                         try: | ||||||
|  |                             file_still_exists = filepath.exists() and filepath.is_file() | ||||||
|  |                         except (PermissionError, OSError):  # pragma: no cover | ||||||
|  |                             # If we can't check, let it fail in the _consume function | ||||||
|  |                             file_still_exists = True | ||||||
|  |                             continue | ||||||
|  |  | ||||||
|                         if waited_long_enough and file_still_exists: |                         if waited_long_enough and file_still_exists: | ||||||
|                             _consume(filepath) |                             _consume(filepath) | ||||||
|   | |||||||
| @@ -92,6 +92,9 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand): | |||||||
|                 # doc to doc is obviously not useful |                 # doc to doc is obviously not useful | ||||||
|                 if first_doc.pk == second_doc.pk: |                 if first_doc.pk == second_doc.pk: | ||||||
|                     continue |                     continue | ||||||
|  |                 # Skip empty documents (e.g. password-protected) | ||||||
|  |                 if first_doc.content.strip() == "" or second_doc.content.strip() == "": | ||||||
|  |                     continue | ||||||
|                 # Skip matching which have already been matched together |                 # Skip matching which have already been matched together | ||||||
|                 # doc 1 to doc 2 is the same as doc 2 to doc 1 |                 # doc 1 to doc 2 is the same as doc 2 to doc 1 | ||||||
|                 doc_1_to_doc_2 = (first_doc.pk, second_doc.pk) |                 doc_1_to_doc_2 = (first_doc.pk, second_doc.pk) | ||||||
|   | |||||||
| @@ -314,11 +314,19 @@ def consumable_document_matches_workflow( | |||||||
|         trigger_matched = False |         trigger_matched = False | ||||||
|  |  | ||||||
|     # Document path vs trigger path |     # Document path vs trigger path | ||||||
|  |  | ||||||
|  |     # Use the original_path if set, else us the original_file | ||||||
|  |     match_against = ( | ||||||
|  |         document.original_path | ||||||
|  |         if document.original_path is not None | ||||||
|  |         else document.original_file | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     if ( |     if ( | ||||||
|         trigger.filter_path is not None |         trigger.filter_path is not None | ||||||
|         and len(trigger.filter_path) > 0 |         and len(trigger.filter_path) > 0 | ||||||
|         and not fnmatch( |         and not fnmatch( | ||||||
|             document.original_file, |             match_against, | ||||||
|             trigger.filter_path, |             trigger.filter_path, | ||||||
|         ) |         ) | ||||||
|     ): |     ): | ||||||
|   | |||||||
| @@ -614,14 +614,16 @@ class TestBarcodeNewConsume( | |||||||
|             self.assertIsNotFile(temp_copy) |             self.assertIsNotFile(temp_copy) | ||||||
|  |  | ||||||
|             # Check the split files exist |             # Check the split files exist | ||||||
|  |             # Check the original_path is set | ||||||
|             # Check the source is unchanged |             # Check the source is unchanged | ||||||
|             # Check the overrides are unchanged |             # Check the overrides are unchanged | ||||||
|             for ( |             for ( | ||||||
|                 new_input_doc, |                 new_input_doc, | ||||||
|                 new_doc_overrides, |                 new_doc_overrides, | ||||||
|             ) in self.get_all_consume_delay_call_args(): |             ) in self.get_all_consume_delay_call_args(): | ||||||
|                 self.assertEqual(new_input_doc.source, DocumentSource.ConsumeFolder) |  | ||||||
|                 self.assertIsFile(new_input_doc.original_file) |                 self.assertIsFile(new_input_doc.original_file) | ||||||
|  |                 self.assertEqual(new_input_doc.original_path, temp_copy) | ||||||
|  |                 self.assertEqual(new_input_doc.source, DocumentSource.ConsumeFolder) | ||||||
|                 self.assertEqual(overrides, new_doc_overrides) |                 self.assertEqual(overrides, new_doc_overrides) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -209,6 +209,26 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase): | |||||||
|         # assert that we have an error logged with this invalid file. |         # assert that we have an error logged with this invalid file. | ||||||
|         error_logger.assert_called_once() |         error_logger.assert_called_once() | ||||||
|  |  | ||||||
|  |     @mock.patch("documents.management.commands.document_consumer.logger.warning") | ||||||
|  |     def test_permission_error_on_prechecks(self, warning_logger): | ||||||
|  |         filepath = Path(self.dirs.consumption_dir) / "selinux.txt" | ||||||
|  |         filepath.touch() | ||||||
|  |  | ||||||
|  |         original_stat = Path.stat | ||||||
|  |  | ||||||
|  |         def raising_stat(self, *args, **kwargs): | ||||||
|  |             if self == filepath: | ||||||
|  |                 raise PermissionError("Permission denied") | ||||||
|  |             return original_stat(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         with mock.patch("pathlib.Path.stat", new=raising_stat): | ||||||
|  |             document_consumer._consume(filepath) | ||||||
|  |  | ||||||
|  |         warning_logger.assert_called_once() | ||||||
|  |         (args, _) = warning_logger.call_args | ||||||
|  |         self.assertIn("Permission denied", args[0]) | ||||||
|  |         self.consume_file_mock.assert_not_called() | ||||||
|  |  | ||||||
|     @override_settings(CONSUMPTION_DIR="does_not_exist") |     @override_settings(CONSUMPTION_DIR="does_not_exist") | ||||||
|     def test_consumption_directory_invalid(self): |     def test_consumption_directory_invalid(self): | ||||||
|         self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") |         self.assertRaises(CommandError, call_command, "document_consumer", "--oneshot") | ||||||
|   | |||||||
| @@ -206,3 +206,29 @@ class TestFuzzyMatchCommand(TestCase): | |||||||
|         self.assertEqual(Document.objects.count(), 2) |         self.assertEqual(Document.objects.count(), 2) | ||||||
|         self.assertIsNotNone(Document.objects.get(pk=1)) |         self.assertIsNotNone(Document.objects.get(pk=1)) | ||||||
|         self.assertIsNotNone(Document.objects.get(pk=2)) |         self.assertIsNotNone(Document.objects.get(pk=2)) | ||||||
|  |  | ||||||
|  |     def test_empty_content(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - 2 documents exist, content is empty (pw-protected) | ||||||
|  |         WHEN: | ||||||
|  |             - Command is called | ||||||
|  |         THEN: | ||||||
|  |             - No matches are found | ||||||
|  |         """ | ||||||
|  |         Document.objects.create( | ||||||
|  |             checksum="BEEFCAFE", | ||||||
|  |             title="A", | ||||||
|  |             content="", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |             filename="test.pdf", | ||||||
|  |         ) | ||||||
|  |         Document.objects.create( | ||||||
|  |             checksum="DEADBEAF", | ||||||
|  |             title="A", | ||||||
|  |             content="", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |             filename="other_test.pdf", | ||||||
|  |         ) | ||||||
|  |         stdout, _ = self.call_command() | ||||||
|  |         self.assertIn("No matches found", stdout) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-09-17 22:44+0000\n" | "POT-Creation-Date: 2025-09-22 18:20+0000\n" | ||||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | "PO-Revision-Date: 2022-02-17 04:17\n" | ||||||
| "Last-Translator: \n" | "Last-Translator: \n" | ||||||
| "Language-Team: English\n" | "Language-Team: English\n" | ||||||
| @@ -1827,7 +1827,7 @@ msgstr "" | |||||||
| msgid "Chinese Traditional" | msgid "Chinese Traditional" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: paperless/urls.py:368 | #: paperless/urls.py:370 | ||||||
| msgid "Paperless-ngx administration" | msgid "Paperless-ngx administration" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -922,7 +922,7 @@ CELERY_ACCEPT_CONTENT = ["application/json", "application/x-python-serialize"] | |||||||
| CELERY_BEAT_SCHEDULE = _parse_beat_schedule() | CELERY_BEAT_SCHEDULE = _parse_beat_schedule() | ||||||
|  |  | ||||||
| # https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule-filename | # https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-schedule-filename | ||||||
| CELERY_BEAT_SCHEDULE_FILENAME = DATA_DIR / "celerybeat-schedule.db" | CELERY_BEAT_SCHEDULE_FILENAME = str(DATA_DIR / "celerybeat-schedule.db") | ||||||
|  |  | ||||||
|  |  | ||||||
| # Cachalot: Database read cache. | # Cachalot: Database read cache. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user