safeuploads 1.1.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. safeuploads-1.1.1/.github/CODEOWNERS +4 -0
  2. safeuploads-1.1.1/.github/FUNDING.yml +13 -0
  3. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/PULL_REQUEST_TEMPLATE.md +1 -1
  4. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/copilot-instructions.md +1 -1
  5. safeuploads-1.1.1/.github/dependabot.yml +56 -0
  6. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/scripts/check_conventional_commits.py +3 -3
  7. safeuploads-1.1.1/.github/scripts/smoke_import.py +35 -0
  8. safeuploads-1.1.1/.github/workflows/audit-safeuploads.yml +58 -0
  9. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/workflows/conventional-commits.yml +11 -8
  10. safeuploads-1.1.1/.github/workflows/docs.yml +81 -0
  11. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/workflows/fuzz.yml +9 -6
  12. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/workflows/lint-and-test.yml +16 -11
  13. safeuploads-1.1.1/.github/workflows/mirror.yml +30 -0
  14. safeuploads-1.1.1/.github/workflows/publish-safeuploads.yml +170 -0
  15. {safeuploads-1.1.0 → safeuploads-1.1.1}/CHANGELOG.md +21 -7
  16. {safeuploads-1.1.0 → safeuploads-1.1.1}/CONTRIBUTING.md +2 -2
  17. {safeuploads-1.1.0 → safeuploads-1.1.1}/PKG-INFO +6 -6
  18. {safeuploads-1.1.0 → safeuploads-1.1.1}/README.md +5 -5
  19. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/index.md +8 -8
  20. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/rate-limiting.md +1 -1
  21. {safeuploads-1.1.0 → safeuploads-1.1.1}/mkdocs.yml +1 -1
  22. {safeuploads-1.1.0 → safeuploads-1.1.1}/pyproject.toml +3 -3
  23. {safeuploads-1.1.0 → safeuploads-1.1.1}/uv.lock +269 -259
  24. safeuploads-1.1.0/.forgejo/workflows/docs.yml +0 -59
  25. safeuploads-1.1.0/.forgejo/workflows/publish.yml +0 -60
  26. safeuploads-1.1.0/.forgejo/workflows/renovate.yml +0 -32
  27. safeuploads-1.1.0/renovate.json +0 -35
  28. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/ISSUE_TEMPLATE/config.yml +0 -0
  30. {safeuploads-1.1.0/.forgejo → safeuploads-1.1.1/.github}/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/agents/se-gitops-ci-specialist.agent.md +0 -0
  32. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/agents/se-security-reviewer.agent.md +0 -0
  33. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/agents/test-implementor.agent.md +0 -0
  34. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/agents/test-reviewer.agent.md +0 -0
  35. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/agents.instructions.md +0 -0
  36. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/github-actions-ci-cd-best-practices.instructions.md +0 -0
  37. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/instructions.instructions.md +0 -0
  38. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/prompt.instructions.md +0 -0
  39. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/python.instructions.md +0 -0
  40. {safeuploads-1.1.0 → safeuploads-1.1.1}/.github/instructions/security-and-owasp.instructions.md +0 -0
  41. {safeuploads-1.1.0 → safeuploads-1.1.1}/.gitignore +0 -0
  42. {safeuploads-1.1.0 → safeuploads-1.1.1}/CODE_OF_CONDUCT.md +0 -0
  43. {safeuploads-1.1.0 → safeuploads-1.1.1}/LICENSE.md +0 -0
  44. {safeuploads-1.1.0 → safeuploads-1.1.1}/SECURITY.md +0 -0
  45. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/api.md +0 -0
  46. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/security/architecture.md +0 -0
  47. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/security/integration-checklist.md +0 -0
  48. {safeuploads-1.1.0 → safeuploads-1.1.1}/docs/security/threat-model.md +0 -0
  49. {safeuploads-1.1.0 → safeuploads-1.1.1}/examples/README.md +0 -0
  50. {safeuploads-1.1.0 → safeuploads-1.1.1}/examples/fastapi_example.py +0 -0
  51. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/__init__.py +0 -0
  52. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/audit.py +0 -0
  53. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/config.py +0 -0
  54. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/enums.py +0 -0
  55. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/exceptions.py +0 -0
  56. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/file_validator.py +0 -0
  57. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/inspectors/__init__.py +0 -0
  58. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/inspectors/base.py +0 -0
  59. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/inspectors/content_inspector.py +0 -0
  60. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/inspectors/gzip_inspector.py +0 -0
  61. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/inspectors/zip_inspector.py +0 -0
  62. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/protocols.py +0 -0
  63. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/py.typed +0 -0
  64. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/utils.py +0 -0
  65. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/__init__.py +0 -0
  66. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/base.py +0 -0
  67. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/compression_validator.py +0 -0
  68. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/extension_validator.py +0 -0
  69. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/unicode_validator.py +0 -0
  70. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/windows_validator.py +0 -0
  71. {safeuploads-1.1.0 → safeuploads-1.1.1}/safeuploads/validators/xml_validator.py +0 -0
  72. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/__init__.py +0 -0
  73. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/conftest.py +0 -0
  74. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/fuzz/__init__.py +0 -0
  75. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/fuzz/test_fuzz_config.py +0 -0
  76. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/fuzz/test_fuzz_filenames.py +0 -0
  77. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/fuzz/test_fuzz_images.py +0 -0
  78. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/fuzz/test_fuzz_zip.py +0 -0
  79. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/inspectors/__init__.py +0 -0
  80. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/inspectors/test_content_inspector.py +0 -0
  81. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/inspectors/test_gzip_inspector.py +0 -0
  82. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/inspectors/test_zip_inspector.py +0 -0
  83. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_audit.py +0 -0
  84. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_config.py +0 -0
  85. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_config_validation.py +0 -0
  86. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_enums.py +0 -0
  87. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_exceptions.py +0 -0
  88. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_fastapi_integration.py +0 -0
  89. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_file_validator.py +0 -0
  90. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_performance.py +0 -0
  91. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_utils.py +0 -0
  92. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_validate_activity_file.py +0 -0
  93. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/test_validate_gzip_file.py +0 -0
  94. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/__init__.py +0 -0
  95. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/test_compression_validator.py +0 -0
  96. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/test_extension_validator.py +0 -0
  97. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/test_unicode_validator.py +0 -0
  98. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/test_windows_validator.py +0 -0
  99. {safeuploads-1.1.0 → safeuploads-1.1.1}/tests/validators/test_xml_validator.py +0 -0
@@ -0,0 +1,4 @@
1
+ # Every change needs a review from the maintainer. safeuploads is maintained by one
2
+ # person (see CONTRIBUTING.md), so this is a single entry rather than a routing
3
+ # table — it exists so review is requested automatically rather than remembered.
4
+ * @joaovitoriasilva
@@ -0,0 +1,13 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: joaovitoriasilva # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4
+ patreon: endurain # Replace with a single Patreon username
5
+ #open_collective: # Replace with a single Open Collective username
6
+ #ko_fi: # Replace with a single Ko-fi username
7
+ #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: endurain # Replace with a single Liberapay username
10
+ #issuehunt: # Replace with a single IssueHunt username
11
+ #otechie: # Replace with a single Otechie username
12
+ #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13
+ custom: ['https://buymeacoffee.com/endurain'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -30,4 +30,4 @@ Check all that apply
30
30
 
31
31
  ## Notes for the reviewer
32
32
 
33
- Anything that would help review this. Tricky areas, alternative approaches considered, screenshots, etc.
33
+ Anything that would help review this. Tricky areas, alternative approaches considered, screenshots, etc.
@@ -77,7 +77,7 @@ Committing should use clear messages following [Conventional Commits](https://ww
77
77
 
78
78
  **Format:** `<type>(<scope>)!: <description>` — `(<scope>)` and the breaking-change `!` are optional.
79
79
 
80
- The following rules are enforced automatically on every PR (against the PR title and every commit subject) by `.forgejo/workflows/conventional-commits.yml`. Validation follows the [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) spec, with a single project-policy addition (the allowed-type whitelist). Generated commit messages must comply:
80
+ The following rules are enforced automatically on every PR (against the PR title and every commit subject) by `.github/workflows/conventional-commits.yml`. Validation follows the [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) spec, with a single project-policy addition (the allowed-type whitelist). Generated commit messages must comply:
81
81
 
82
82
  - **Header format:** `<type>(<scope>)!: <description>` — the `(<scope>)` and breaking-change `!` are optional, and a space is required after the colon.
83
83
  - **Allowed types (project policy):** `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`. Types are matched case-insensitively per the spec.
@@ -0,0 +1,56 @@
1
+ # Dependabot keeps dependencies *fresh*; the separate `audit-safeuploads` workflow
2
+ # is what catches an advisory against a version already pinned. Both are needed.
3
+ #
4
+ # Updates are grouped so a week's worth of patch bumps arrives as one reviewable
5
+ # pull request rather than a dozen. `uv.lock` is regenerated by Dependabot's uv
6
+ # support, so the lockfile and `pyproject.toml` stay in step.
7
+ version: 2
8
+
9
+ updates:
10
+ - package-ecosystem: uv
11
+ directory: /
12
+ schedule:
13
+ interval: weekly
14
+ day: monday
15
+ time: "05:00"
16
+ timezone: Etc/UTC
17
+ open-pull-requests-limit: 5
18
+ labels:
19
+ - dependencies
20
+ commit-message:
21
+ # Matches the Conventional Commits check in `conventional-commits.yml`.
22
+ prefix: chore
23
+ include: scope
24
+ groups:
25
+ # The runtime dependency set is small and deliberate, so these are worth
26
+ # reading individually rather than batching.
27
+ production-dependencies:
28
+ dependency-type: production
29
+ update-types:
30
+ - minor
31
+ - patch
32
+ development-dependencies:
33
+ dependency-type: development
34
+ update-types:
35
+ - minor
36
+ - patch
37
+
38
+ # Every action is pinned to a commit SHA, so without this they never move.
39
+ - package-ecosystem: github-actions
40
+ directory: /
41
+ schedule:
42
+ interval: weekly
43
+ day: monday
44
+ time: "05:30"
45
+ timezone: Etc/UTC
46
+ open-pull-requests-limit: 5
47
+ labels:
48
+ - dependencies
49
+ - github-actions
50
+ commit-message:
51
+ prefix: chore
52
+ include: scope
53
+ groups:
54
+ github-actions:
55
+ patterns:
56
+ - "*"
@@ -12,9 +12,9 @@ Usage::
12
12
  check_conventional_commits.py "feat(api): add endpoint"
13
13
  git log --format=%s base..head | check_conventional_commits.py --stdin
14
14
 
15
- Designed to run with the Python interpreter already available on the Forgejo
16
- runner image. Uses only the standard library so there is no third-party
17
- supply-chain surface.
15
+ Designed to run with the Python interpreter already available on the GitHub
16
+ Actions runner image. Uses only the standard library so there is no
17
+ third-party supply-chain surface.
18
18
  """
19
19
 
20
20
  from __future__ import annotations
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ """Smoke-test that the built safeuploads wheel is self-contained.
3
+
4
+ Imports the installed package (not the source tree) and constructs
5
+ a FileValidator to prove the distribution's declared dependencies
6
+ are sufficient to actually use the library.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from importlib.metadata import version
13
+
14
+ from safeuploads import FileSecurityConfig, FileValidator
15
+
16
+
17
+ def main() -> int:
18
+ """Exercise the installed package and return a process exit code.
19
+
20
+ Returns:
21
+ 0 on success, 1 if a check fails.
22
+ """
23
+ print(f"safeuploads version: {version('safeuploads')}")
24
+
25
+ validator = FileValidator(config=FileSecurityConfig())
26
+ if not isinstance(validator, FileValidator):
27
+ print("FileValidator construction failed.", file=sys.stderr)
28
+ return 1
29
+
30
+ print("Smoke test passed: package imported and validator built.")
31
+ return 0
32
+
33
+
34
+ if __name__ == "__main__":
35
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ name: Audit dependencies
2
+
3
+ # Dependabot keeps dependencies *fresh*; it does not tell you when a version you
4
+ # are already pinned to turns out to be vulnerable. This job scans the resolved
5
+ # lockfile against the OSV / PyPI advisory databases, on every dependency change
6
+ # and on a schedule so a newly-published advisory against an unchanged pin is
7
+ # still surfaced.
8
+
9
+ on:
10
+ push:
11
+ branches:
12
+ - main
13
+ paths: &paths
14
+ - pyproject.toml
15
+ - uv.lock
16
+ - .github/workflows/audit-safeuploads.yml
17
+ pull_request:
18
+ paths: *paths
19
+ schedule:
20
+ # Daily, offset from the dependency-update run so the two do not contend.
21
+ - cron: '30 5 * * *'
22
+ workflow_dispatch:
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ concurrency:
28
+ group: ${{ github.workflow }}-${{ github.ref }}
29
+ cancel-in-progress: true
30
+
31
+ jobs:
32
+ audit:
33
+ name: safeuploads (pip-audit)
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - name: Checkout code
37
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+ with:
39
+ fetch-depth: 1
40
+
41
+ - name: Install uv
42
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
43
+ with:
44
+ # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
45
+ # and local development are held to the same floor and ceiling.
46
+ version-file: pyproject.toml
47
+
48
+ # Audit the full resolved set - runtime dependencies plus every optional
49
+ # extra - because a host enabling ``safeuploads[fastapi]`` inherits those too.
50
+ # ``--no-emit-project`` drops the editable self-reference, which carries no
51
+ # hash and which pip-audit therefore rejects.
52
+ - name: Export the resolved dependency set
53
+ run: >-
54
+ uv export --frozen --all-extras --no-dev --no-emit-project
55
+ --format requirements-txt --output-file "$RUNNER_TEMP/requirements.txt"
56
+
57
+ - name: Scan for known vulnerabilities
58
+ run: uvx pip-audit --strict --requirement "$RUNNER_TEMP/requirements.txt" --disable-pip
@@ -5,16 +5,19 @@ on:
5
5
  types: [opened, edited, synchronize, reopened]
6
6
 
7
7
  concurrency:
8
- group: ${{ forgejo.workflow }}-${{ forgejo.event.pull_request.number }}
8
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
9
9
  cancel-in-progress: true
10
10
 
11
+ permissions:
12
+ contents: read
13
+
11
14
  jobs:
12
15
  validate:
13
- runs-on: local-runner
16
+ runs-on: ubuntu-latest
14
17
 
15
18
  steps:
16
19
  - name: Checkout code
17
- uses: https://data.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
18
21
  with:
19
22
  fetch-depth: 0
20
23
 
@@ -22,19 +25,19 @@ jobs:
22
25
  # The PR title is passed through an environment variable to prevent
23
26
  # shell injection via crafted titles (see security-and-owasp guidelines).
24
27
  env:
25
- PR_TITLE: ${{ forgejo.event.pull_request.title }}
28
+ PR_TITLE: ${{ github.event.pull_request.title }}
26
29
  run: |
27
- python3 .forgejo/scripts/check_conventional_commits.py "$PR_TITLE"
30
+ python3 .github/scripts/check_conventional_commits.py "$PR_TITLE"
28
31
 
29
32
  - name: Validate commits in PR
30
33
  # Validates every commit subject between the PR base and head. Use the
31
34
  # base/head SHAs from the event payload so we don't depend on the local
32
35
  # checkout ref layout.
33
36
  env:
34
- BASE_SHA: ${{ forgejo.event.pull_request.base.sha }}
35
- HEAD_SHA: ${{ forgejo.event.pull_request.head.sha }}
37
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
38
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
36
39
  run: |
37
40
  set -euo pipefail
38
41
  git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA"
39
42
  git log --no-merges --format=%s "$BASE_SHA".."$HEAD_SHA" \
40
- | python3 .forgejo/scripts/check_conventional_commits.py --stdin
43
+ | python3 .github/scripts/check_conventional_commits.py --stdin
@@ -0,0 +1,81 @@
1
+ name: Deploy documentation
2
+
3
+ # ``dry-run`` only exists on ``workflow_dispatch``. On a push ``inputs.dry-run``
4
+ # is null, so every ``!= true`` guard below passes and the site deploys.
5
+ on:
6
+ workflow_dispatch:
7
+ inputs:
8
+ dry-run:
9
+ description: Build docs but do not deploy
10
+ required: false
11
+ default: false
12
+ type: boolean
13
+ push:
14
+ branches:
15
+ - main
16
+ paths:
17
+ - 'docs/**'
18
+ - 'mkdocs.yml'
19
+ - 'safeuploads/**/*.py'
20
+ - '.github/workflows/docs.yml'
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ concurrency:
26
+ group: github-pages
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ build:
31
+ name: Build documentation
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ contents: read
35
+ pages: write
36
+ steps:
37
+ - name: Checkout code
38
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
+ with:
40
+ fetch-depth: 1
41
+
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
44
+ with:
45
+ # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
46
+ # and local development are held to the same floor and ceiling.
47
+ version-file: pyproject.toml
48
+
49
+ # mkdocstrings imports the package to render the API reference, so sync the
50
+ # default dependencies plus the docs group and install the project itself.
51
+ - name: Sync documentation dependencies
52
+ run: uv sync --frozen --group docs
53
+
54
+ - name: Build documentation
55
+ run: uv run --no-sync mkdocs build --site-dir site
56
+
57
+ - name: Configure GitHub Pages
58
+ if: ${{ inputs.dry-run != true }}
59
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
60
+
61
+ - name: Upload GitHub Pages artifact
62
+ if: ${{ inputs.dry-run != true }}
63
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
64
+ with:
65
+ path: site
66
+
67
+ deploy:
68
+ name: Deploy documentation
69
+ if: ${{ inputs.dry-run != true }}
70
+ needs: build
71
+ runs-on: ubuntu-latest
72
+ permissions:
73
+ pages: write
74
+ id-token: write
75
+ environment:
76
+ name: github-pages
77
+ url: ${{ steps.deployment.outputs.page_url }}
78
+ steps:
79
+ - name: Deploy to GitHub Pages
80
+ id: deployment
81
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
@@ -6,28 +6,31 @@ on:
6
6
  - cron: "0 3 * * 0"
7
7
  workflow_dispatch:
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  # Cancel in-progress runs for the same branch to avoid wasted
10
13
  # runner minutes.
11
14
  concurrency:
12
- group: ${{ forge.workflow }}-${{ forge.ref }}
15
+ group: ${{ github.workflow }}-${{ github.ref }}
13
16
  cancel-in-progress: true
14
17
 
15
18
  jobs:
16
19
  fuzz:
17
20
  name: Fuzz
18
- runs-on: local-runner
21
+ runs-on: ubuntu-latest
19
22
 
20
23
  steps:
21
24
  - name: Checkout repository
22
- uses: https://data.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
26
 
24
27
  - name: Install system dependencies
25
- run: apt-get update && apt-get install -y libmagic1
28
+ run: sudo apt-get update && sudo apt-get install -y libmagic1
26
29
 
27
30
  - name: Install uv
28
- uses: https://data.forgejo.org/astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
31
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
32
  with:
30
- working-directory: .
33
+ version-file: pyproject.toml
31
34
 
32
35
  - name: Install dependencies
33
36
  run: uv sync --frozen --dev
@@ -6,10 +6,13 @@ on:
6
6
  pull_request:
7
7
  branches: [main]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  # Cancel in-progress runs for the same branch/PR to avoid wasted
10
13
  # runner minutes and race conditions on shared resources.
11
14
  concurrency:
12
- group: ${{ forge.workflow }}-${{ forge.ref }}
15
+ group: ${{ github.workflow }}-${{ github.ref }}
13
16
  cancel-in-progress: true
14
17
 
15
18
  jobs:
@@ -20,18 +23,20 @@ jobs:
20
23
  # ----------------------------------------------------------------
21
24
  lint:
22
25
  name: Lint
23
- runs-on: codeberg-tiny
26
+ runs-on: ubuntu-latest
24
27
 
25
28
  steps:
26
29
  - name: Checkout code
27
- uses: https://data.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28
31
  with:
29
32
  fetch-depth: 1
30
33
 
31
34
  - name: Install uv
32
- uses: https://data.forgejo.org/astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
35
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
33
36
  with:
34
- working-directory: .
37
+ # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
38
+ # and local development are held to the same floor and ceiling.
39
+ version-file: pyproject.toml
35
40
 
36
41
  - name: Install dependencies
37
42
  run: uv sync --frozen --dev
@@ -52,20 +57,20 @@ jobs:
52
57
  # ----------------------------------------------------------------
53
58
  test:
54
59
  name: Test
55
- runs-on: local-runner
60
+ runs-on: ubuntu-latest
56
61
  needs: lint
57
62
 
58
63
  steps:
59
64
  - name: Checkout repository
60
- uses: https://data.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
65
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
61
66
 
62
67
  - name: Install system dependencies
63
- run: apt-get update && apt-get install -y libmagic1
68
+ run: sudo apt-get update && sudo apt-get install -y libmagic1
64
69
 
65
70
  - name: Install uv
66
- uses: https://data.forgejo.org/astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
71
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
67
72
  with:
68
- working-directory: .
73
+ version-file: pyproject.toml
69
74
 
70
75
  - name: Install dependencies
71
76
  run: uv sync --frozen --dev --extra fastapi
@@ -77,7 +82,7 @@ jobs:
77
82
  run: uv run pytest --cov-fail-under=90
78
83
 
79
84
  - name: Upload coverage report
80
- uses: https://data.forgejo.org/forgejo/upload-artifact@v4
85
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
81
86
  if: always()
82
87
  with:
83
88
  name: safeuploads-coverage
@@ -0,0 +1,30 @@
1
+ name: Mirror to GitLab
2
+
3
+ on:
4
+ push:
5
+
6
+ concurrency:
7
+ group: gitlab-mirror
8
+ cancel-in-progress: false
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ mirror:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Push full mirror to GitLab
18
+ env:
19
+ GITHUB_TOKEN: ${{ github.token }}
20
+ GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
21
+ run: |
22
+ set -euo pipefail
23
+ mirror_dir="$RUNNER_TEMP/safeuploads.git"
24
+ git clone --mirror \
25
+ "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
26
+ "$mirror_dir"
27
+ cd "$mirror_dir"
28
+ git remote set-url --push origin \
29
+ "https://oauth2:${GITLAB_TOKEN}@gitlab.com/endurain-project/safeuploads.git"
30
+ git push --mirror
@@ -0,0 +1,170 @@
1
+ name: Publish safeuploads
2
+
3
+ # Builds and publishes a release to PyPI when a ``v*`` tag is pushed. The
4
+ # unprivileged build job validates the release and uploads its artifacts; the
5
+ # separate ``pypi`` environment job receives OIDC permission only when it needs
6
+ # to generate PEP 740 attestations and publish through Trusted Publishing.
7
+
8
+ on:
9
+ push:
10
+ tags:
11
+ - 'v*'
12
+ workflow_dispatch:
13
+ inputs:
14
+ dry-run:
15
+ description: Build and verify, but do not upload to PyPI
16
+ required: false
17
+ default: true
18
+ type: boolean
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ concurrency:
24
+ group: ${{ github.workflow }}-${{ github.ref }}
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ build:
29
+ name: Build and verify
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Checkout code
33
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+ with:
35
+ fetch-depth: 0
36
+ persist-credentials: false
37
+
38
+ - name: Install uv
39
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
40
+ with:
41
+ # Resolve uv from `[tool.uv] required-version` in pyproject.toml, so CI
42
+ # and local development are held to the same floor and ceiling.
43
+ version-file: pyproject.toml
44
+ enable-cache: false
45
+
46
+ # A tag that disagrees with the packaged version produces a release whose
47
+ # artifact name does not match its tag - catch it before anything is
48
+ # uploaded, because a PyPI version can never be re-used.
49
+ - name: Verify the tag matches the project version
50
+ if: startsWith(github.ref, 'refs/tags/v')
51
+ env:
52
+ TAG_REF: ${{ github.ref }}
53
+ run: |
54
+ set -euo pipefail
55
+ tag_version="${TAG_REF#refs/tags/v}"
56
+ project_version="$(uv version --short)"
57
+ if [ "$tag_version" != "$project_version" ]; then
58
+ echo "Tag v${tag_version} does not match pyproject version ${project_version}." >&2
59
+ exit 1
60
+ fi
61
+ echo "Releasing version ${project_version}."
62
+
63
+ - name: Verify the changelog documents this version
64
+ if: startsWith(github.ref, 'refs/tags/v')
65
+ run: |
66
+ set -euo pipefail
67
+ version="$(uv version --short)"
68
+ if ! grep -q "^## \[${version}\]" CHANGELOG.md; then
69
+ echo "CHANGELOG.md has no '## [${version}]' section." >&2
70
+ exit 1
71
+ fi
72
+
73
+ # The full gate runs on every PR, but a tag can point anywhere in history,
74
+ # so re-run it here: a release must never ship code that does not pass.
75
+ - name: Sync dependencies
76
+ run: uv sync --frozen --dev --extra fastapi
77
+
78
+ - name: Lint, type check and test
79
+ run: |
80
+ set -euo pipefail
81
+ uv run --no-sync ruff check safeuploads tests examples
82
+ uv run --no-sync ruff format --check safeuploads tests examples
83
+ uv run --no-sync mypy
84
+ uv run --no-sync pytest --cov-fail-under=90
85
+
86
+ - name: Build sdist and wheel
87
+ run: uv build --no-sources
88
+
89
+ - name: Verify package metadata renders on PyPI
90
+ run: uvx twine check --strict dist/*
91
+
92
+ # Prove the wheel is self-contained: install it into a clean environment
93
+ # with no dev groups and no optional extras, then import and assemble it
94
+ # from outside the repository (so the source tree cannot shadow it).
95
+ - name: Smoke-test the built wheel
96
+ run: |
97
+ set -euo pipefail
98
+ smoke_venv="$RUNNER_TEMP/smoke"
99
+ uv venv "$smoke_venv"
100
+ VIRTUAL_ENV="$smoke_venv" uv pip install dist/*.whl
101
+ cd "$RUNNER_TEMP"
102
+ VIRTUAL_ENV="$smoke_venv" uv run --no-project python "$GITHUB_WORKSPACE/.github/scripts/smoke_import.py"
103
+
104
+ # An SBOM of what the *distribution* actually pulls in, taken from the
105
+ # clean venv the smoke test just installed the wheel into. Running it
106
+ # against the build environment instead would describe this workflow's own
107
+ # toolchain — ruff, mypy, pytest — which is not what a consumer inherits.
108
+ - name: Generate SBOM (CycloneDX)
109
+ run: |
110
+ set -euo pipefail
111
+ mkdir -p release-metadata
112
+ uvx --from cyclonedx-bom cyclonedx-py environment "$RUNNER_TEMP/smoke" \
113
+ --output-format json --output-file release-metadata/safeuploads-sbom.cdx.json
114
+
115
+ - name: Generate checksums
116
+ run: |
117
+ set -euo pipefail
118
+ sha256sum dist/* > release-metadata/SHA256SUMS
119
+ cat release-metadata/SHA256SUMS
120
+
121
+ # ``dist/`` holds the wheel and the sdist and nothing else: the publish job
122
+ # attests everything it downloads, so a stray checksum or SBOM file there
123
+ # would end up in the attestation.
124
+ - name: Upload build artifacts
125
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
126
+ with:
127
+ name: safeuploads-dist
128
+ path: dist/
129
+ if-no-files-found: error
130
+ retention-days: 90
131
+
132
+ - name: Upload release metadata
133
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
134
+ with:
135
+ name: safeuploads-release-metadata
136
+ path: release-metadata/
137
+ if-no-files-found: error
138
+ retention-days: 90
139
+
140
+ publish:
141
+ name: Publish to PyPI
142
+ if: ${{ startsWith(github.ref, 'refs/tags/v') && inputs.dry-run != true }}
143
+ needs: build
144
+ runs-on: ubuntu-latest
145
+ environment:
146
+ name: pypi
147
+ url: https://pypi.org/p/safeuploads
148
+ permissions:
149
+ id-token: write
150
+ steps:
151
+ - name: Install uv
152
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
153
+ with:
154
+ # This job never checks the repository out, so there is no
155
+ # pyproject.toml to read `required-version` from; keep this in step
156
+ # with the floor declared there.
157
+ version: "0.12.5"
158
+ enable-cache: false
159
+
160
+ - name: Download build artifacts
161
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
162
+ with:
163
+ name: safeuploads-dist
164
+ path: dist/
165
+
166
+ - name: Generate PEP 740 attestations
167
+ uses: astral-sh/attest-action@f589a42a7efb6fe400b4f400de60b4bc90390027 # v0.0.6
168
+
169
+ - name: Publish to PyPI
170
+ run: uv publish --trusted-publishing always dist/*.whl dist/*.tar.gz