diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 77bea4917..21406ad62 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -40,3 +40,4 @@ NOTE: PRs that do not address the following will not be merged, please do not sk - [ ] I have run all Git `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks). - [ ] I have made corresponding changes to the documentation as needed. - [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR. +- [ ] I have added a changelog fragment in `docs/changelog.d/` (see [contributing guidelines](CONTRIBUTING.md#changelog-fragments)), or this PR does not require one (bot PR or labeled `skip-changelog`). diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 2b8169f24..000000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,54 +0,0 @@ -categories: - - title: 'Breaking Changes' - labels: - - 'breaking-change' - - title: 'Notable Changes' - labels: - - 'notable' - - title: 'Features / Enhancements' - labels: - - 'enhancement' - - title: 'Bug Fixes' - labels: - - 'bug' - - title: 'Documentation' - labels: - - 'documentation' - - title: 'Maintenance' - labels: - - 'chore' - - 'deployment' - - 'translation' - - 'ci-cd' - - title: 'Dependencies' - collapse-after: 3 - labels: - - 'dependencies' - - title: 'All App Changes' - labels: - - 'frontend' - - 'backend' - collapse-after: 1 -include-labels: - - 'enhancement' - - 'bug' - - 'chore' - - 'deployment' - - 'translation' - - 'dependencies' - - 'documentation' - - 'frontend' - - 'backend' - - 'ci-cd' - - 'breaking-change' - - 'notable' -exclude-labels: - - 'skip-changelog' -filter-by-commitish: true -category-template: '### $TITLE' -change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))' -change-title-escapes: '\<*_&#@' -template: | - ## paperless-ngx $RESOLVED_VERSION - - $CHANGES diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 8aace2168..090daa992 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -26,3 +26,33 @@ jobs: python-version: "3.14" - name: Run prek uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 + - name: Fetch base branch for fragment check + if: | + github.event_name == 'pull_request' && + !contains(github.actor, '[bot]') && + !contains(join(github.event.pull_request.labels.*.name, ','), 'skip-changelog') + run: git fetch origin ${{ github.base_ref }} --depth=1 + - name: Check changelog fragment + if: | + github.event_name == 'pull_request' && + !contains(github.actor, '[bot]') && + !contains(join(github.event.pull_request.labels.*.name, ','), 'skip-changelog') + run: | + pip install towncrier + towncrier check --compare-with origin/${{ github.base_ref }} + - name: Check fragment is single line + if: | + github.event_name == 'pull_request' && + !contains(github.actor, '[bot]') && + !contains(join(github.event.pull_request.labels.*.name, ','), 'skip-changelog') + run: | + failed=0 + while IFS= read -r fragment; do + lines=$(grep -c . "$fragment" || true) + if [ "$lines" -ne 1 ]; then + echo "Error: $fragment must contain exactly one line (found $lines)" + failed=1 + fi + done < <(git diff --name-only --diff-filter=AM "origin/${{ github.base_ref }}...HEAD" \ + | grep -E '^docs/changelog\.d/[0-9]+\.[a-z]+\.md$') + exit $failed diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 865609c40..60f3d9f95 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -149,7 +149,6 @@ jobs: pull-requests: write outputs: prerelease: ${{ steps.get-version.outputs.prerelease }} - changelog: ${{ steps.create-release.outputs.body }} version: ${{ steps.get-version.outputs.version }} steps: - name: Download release artifact @@ -168,26 +167,42 @@ jobs: else echo "prerelease=false" >> $GITHUB_OUTPUT fi - - name: Create release and changelog - id: create-release - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 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 - commitish: main + persist-credentials: true + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: ${{ env.DEFAULT_UV_VERSION }} + - name: Draft release notes env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Upload release archive - uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1.10.1 - 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 + VERSION: ${{ steps.get-version.outputs.version }} + run: | + uv run --group changelog towncrier build \ + --draft \ + --version "${VERSION#v}" \ + > RELEASE_NOTES.md + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get-version.outputs.version }} + PRERELEASE: ${{ steps.get-version.outputs.prerelease }} + run: | + prerelease_flag="" + if [[ "${PRERELEASE}" == "true" ]]; then + prerelease_flag="--prerelease" + fi + mv paperless-ngx.tar.xz "paperless-ngx-${VERSION}.tar.xz" + gh release create "${VERSION}" \ + --title "Paperless-ngx ${VERSION}" \ + --notes-file RELEASE_NOTES.md \ + ${prerelease_flag} \ + "./paperless-ngx-${VERSION}.tar.xz" # --------------------------------------------------------------------------- # Append changelog to docs (only on non-prerelease) # --------------------------------------------------------------------------- @@ -216,38 +231,24 @@ jobs: version: ${{ env.DEFAULT_UV_VERSION }} enable-cache: false python-version: ${{ env.DEFAULT_PYTHON_VERSION }} - - name: Update changelog - working-directory: docs + - name: Build changelog + env: + VERSION: ${{ needs.publish-release.outputs.version }} + run: | + uv run --group changelog towncrier build \ + --version "${VERSION#v}" \ + --yes + uv run --dev prek run --files docs/changelog.md || true + - name: Commit and push env: - CHANGELOG: ${{ needs.publish-release.outputs.changelog }} - PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} VERSION: ${{ needs.publish-release.outputs.version }} run: | branch_name="${VERSION}-changelog" - - git branch "${branch_name}" - git checkout "${branch_name}" - - printf '# Changelog\n\n%s\n' "${CHANGELOG}" > 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 "${PYTHON_VERSION}" \ - --dev \ - prek 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 ${VERSION} - GHA" + git checkout -b "${branch_name}" + git config user.name "github-actions" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/changelog.md docs/changelog.d/ + git commit -m "Changelog ${VERSION} - GHA" git push origin "${branch_name}" - name: Create pull request uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8bc4979c..0aad98a96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,3 +83,7 @@ repos: exclude: "^src-ui/pnpm-lock.yaml" types: - yaml + - repo: https://github.com/twisted/towncrier + rev: 25.8.0 + hooks: + - id: towncrier-check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e5539b2a..0c2ef3e3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,50 @@ To test your code, execute `pytest` in the src/ directory. This also generates a Before you can run `pytest`, ensure to [properly set up your local environment](https://docs.paperless-ngx.com/development/#initial-setup-and-first-start). +### Changelog Fragments + +Every pull request that changes behavior, adds a feature, or fixes a bug +must include a news fragment. Fragments live in `docs/changelog.d/` and are +named `..md`. + +For example, if your PR number is 1234 and it fixes a bug, create +`docs/changelog.d/1234.bugfix.md` with content describing what changed. + +If you haven't opened the PR yet, use a `+` prefix as a placeholder: + +``` +docs/changelog.d/+my-change.bugfix.md +``` + +Rename it to the real PR number once the PR exists. The `+`-prefixed fragment +is valid and will pass CI, but the rendered changelog entry won't include a PR +link until it's renamed. + +**Fragment types:** + +| Type | Changelog section | Use for | +| ---------- | ------------------------ | ------------------------------------------------ | +| `breaking` | Breaking Changes | Anything that breaks existing behavior | +| `notable` | Notable Changes | Significant changes worth calling out separately | +| `feature` | Features / Enhancements | New features and enhancements | +| `bugfix` | Bug Fixes | Bug fixes | +| `doc` | Documentation | Documentation-only changes | +| `misc` | Maintenance (count only) | Chores, CI changes, refactoring | + +Write the fragment content as a complete sentence describing the change +from the user's perspective. You may include attribution at the end: + +``` +Fixed the crash when importing documents with long filenames. By @yourusername +``` + +**You do not need a fragment if:** + +- Your PR is from an automated bot (Dependabot, Crowdin) +- Your PR is labeled `skip-changelog` + +CI will fail on PRs that require a fragment but don't have one. + ## More info: ... is available [in the documentation](https://docs.paperless-ngx.com/development). diff --git a/docs/changelog.d/.gitkeep b/docs/changelog.d/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs/changelog.d/README.md b/docs/changelog.d/README.md new file mode 100644 index 000000000..e2ce04564 --- /dev/null +++ b/docs/changelog.d/README.md @@ -0,0 +1,14 @@ +Add a news fragment for each pull request: + +docs/changelog.d/..md + +Types: breaking, notable, feature, bugfix, doc, misc + +Example: docs/changelog.d/1234.bugfix.md +Content: Fixed the thing that broke when X happened. By @yourusername + +If you haven't opened the PR yet, use a + prefix as a placeholder: + +docs/changelog.d/+my-change.bugfix.md + +Rename it to the real PR number once the PR exists. diff --git a/docs/changelog.md b/docs/changelog.md index 8ec847ae5..ffc85ddf8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,7 @@ # Changelog + + ## paperless-ngx 2.20.15 ### Security diff --git a/pyproject.toml b/pyproject.toml index 9343e7529..f1c974db1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ webserver = [ [dependency-groups] dev = [ + { include-group = "changelog" }, { include-group = "docs" }, { include-group = "lint" }, { include-group = "testing" }, @@ -103,6 +104,9 @@ dev = [ docs = [ "zensical>=0.0.36", ] +changelog = [ + "towncrier>=25.8", +] lint = [ "prek~=0.3.10", "ruff~=0.15.12", @@ -342,6 +346,38 @@ omit = [ "paperless/auth.py", ] +[tool.towncrier] +directory = "docs/changelog.d" +filename = "docs/changelog.md" +title_format = "## paperless-ngx {version}" +issue_format = "[#{issue}](https://github.com/paperless-ngx/paperless-ngx/pull/{issue})" +underlines = [ "", "", "" ] +start_string = "\n" +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true +[[tool.towncrier.type]] +directory = "notable" +name = "Notable Changes" +showcontent = true +[[tool.towncrier.type]] +directory = "feature" +name = "Features / Enhancements" +showcontent = true +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug Fixes" +showcontent = true +[[tool.towncrier.type]] +directory = "doc" +name = "Documentation" +showcontent = true +[[tool.towncrier.type]] +directory = "misc" +name = "Maintenance" +showcontent = false + [tool.mypy-baseline] baseline_path = ".mypy-baseline.txt" sort_baseline = true diff --git a/uv.lock b/uv.lock index 96ac0e6f7..42e2d4615 100644 --- a/uv.lock +++ b/uv.lock @@ -2953,6 +2953,9 @@ webserver = [ ] [package.dev-dependencies] +changelog = [ + { name = "towncrier", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] dev = [ { name = "daphne", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "factory-boy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2970,6 +2973,7 @@ dev = [ { name = "pytest-xdist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "time-machine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "towncrier", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zensical", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] docs = [ @@ -3094,6 +3098,7 @@ requires-dist = [ provides-extras = ["mariadb", "postgres", "webserver"] [package.metadata.requires-dev] +changelog = [{ name = "towncrier", specifier = ">=25.8.0" }] dev = [ { name = "daphne" }, { name = "factory-boy", specifier = "~=3.3.1" }, @@ -3111,6 +3116,7 @@ dev = [ { name = "pytest-xdist", specifier = "~=3.8.0" }, { name = "ruff", specifier = "~=0.15.12" }, { name = "time-machine", specifier = ">=2.13" }, + { name = "towncrier", specifier = ">=25.8.0" }, { name = "zensical", specifier = ">=0.0.36" }, ] docs = [{ name = "zensical", specifier = ">=0.0.36" }] @@ -5015,6 +5021,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, ] +[[package]] +name = "towncrier" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, +] + [[package]] name = "tqdm" version = "4.67.3"