Перейти к содержанию

Contributing

Thanks for your interest! This project welcomes focused contributions. Before you open a PR, please read the policies below — they keep the review cycle short.

Setup

git clone <repo-url>
cd XFerryServer
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,lint,test]"
pre-commit install           # recommended: runs ruff/mypy/yaml checks on commit

On Windows PowerShell, activate the environment with .venv\Scripts\Activate.ps1 and use the same pip install command.

To reproduce the pinned CI/docs/security toolchain locally, use the shared constraints file. This also pins the pre-commit executable installed by the dev extra:

PIP_CONSTRAINT=constraints/ci.txt pip install -e ".[dev,lint,test]"

constraints/ci.txt is the only committed dependency authority for CI, Docker, security, docs, and reproducible local installs. Local uv.lock files are intentionally ignored; if you use uv locally, regenerate its lock from the current pyproject.toml and constraints file instead of committing it.

Supported Python versions are 3.10 through 3.14. Package metadata caps the range below 3.15. CI tests Python 3.14 in the constrained matrix and runs a dedicated readiness job for pip check, import smoke, wheel smoke, and pip-audit with constraints/ci.txt.

In Windows PowerShell, set the constraint as an environment variable:

$env:PIP_CONSTRAINT = "constraints/ci.txt"
pip install -e ".[dev,lint,test]"
Remove-Item Env:PIP_CONSTRAINT

Branching

  • Work from main on a feature branch: feature/<short-slug>, fix/<issue>, security/<id>, docs/<area>.
  • main is protected. All changes land via PR with a passing CI.
  • Rebase, do not merge, when updating your branch from main (git pull --rebase origin main).

Commit messages

Use Conventional Commits:

<type>(<scope>): <short imperative summary>

<body, wrapped at 72 cols, explaining WHY>

Refs: #123

Types we use:

Type When to use
feat New user-facing capability
fix Bug fix — include reproducer in the body
security Security-impacting change — link advisory ID
perf Measurable performance improvement
refactor Internal change, no behavior shift
test Tests only, no production change
docs Documentation only
ci CI/workflow/tooling change
deps Dependency bump (often by Dependabot)
chore Anything else that doesn't change behavior

Breaking changes get a ! after the type (feat!:) and a BREAKING CHANGE: footer.

Running checks locally

The CI (.github/workflows/ci.yml) runs all of these — run them locally first to avoid round-trips.

When you need to reproduce CI package resolution exactly, prefix the install step with PIP_CONSTRAINT=constraints/ci.txt.

Browser smoke is release-gating in CI. Its @playwright/cli package is pinned in .github/workflows/ci.yml and tools/browser_smoke.py; update those pins together when refreshing the smoke toolchain.

pre-commit run --all-files
python -m pip check
python -c "import acme, cryptography, josepy; from OpenSSL import SSL"
python tools/check_dependency_constraints.py --constraints constraints/ci.txt
ruff check xferry src tests
ruff format --check xferry src tests
mypy xferry src
pytest --cov=src --cov-report=term-missing
python tools/sync_docs.py --check
python tools/check_stale_docs.py
python tools/browser_smoke.py --mode first-run
python tools/browser_smoke.py --mode ui-contracts
python tools/browser_smoke.py --mode full
# optional when docs extras are installed
mkdocs build --strict

Coverage gate in CI is 65 %; aim higher.

The independently runnable browser modes are first-run, ui-contracts, request-matrix, advanced, files, notepad, mobile, and aggregate full. To exercise the exact first-run assertions against an already running source, installed-wheel, or image server, use:

python tools/browser_smoke.py \
  --mode first-run \
  --target-url http://127.0.0.1:8080/

This external-target path starts no additional server. Results and failure diagnostics default to output/playwright/browser-smoke/<mode>; pass --artifacts-dir to choose a CI or local artifact directory.

Release artifacts

The Release Artifacts workflow runs on v* tags and manual workflow_dispatch. Neither the public xferry PyPI distribution nor the expected ghcr.io/kgmnotes/xferry release image exists yet; their first coordinated publication is intentionally deferred. Manual runs remain verification-only and never publish to either registry.

The managed VPS installer and latest/download release assets have not been published, and this documentation change does not publish, tag, deploy, or mutate a VPS. Do not describe the two-command installer URL as live until a verified release has actually published its manifest and platform binary.

Publication is downstream of one shared gate. The build job creates and attests dist/*.whl and dist/*.tar.gz, installs the wheel in a fresh virtual environment outside the checkout, runs CLI/import and static-asset probes, and executes both the shared external-target first-run and the installed-package full browser aggregate. In parallel, image-verify builds the hardened local image and runs its lifecycle/browser verification. release-gate succeeds only after both verification jobs.

For a push event on a v* tag, and only after release-gate, publish-pypi and publish-ghcr become eligible to run independently in parallel. A verification failure therefore prevents both registry mutations. Manual runs stop at the gate, even when dispatched against a tag. Browser diagnostics are uploaded with if: always().

The workflow uses explicit permissions: contents: read for checkout, packages: write for GHCR publication, id-token: write for PyPI Trusted Publishing/OIDC, and attestations: write for GitHub artifact attestations. It also scans the pinned constraints/ci.txt dependency set with pip-audit, writes a CycloneDX JSON SBOM into dist/, and attests the wheel, sdist, and SBOM.

PyPI and GHCR do not provide one cross-registry transaction, so parallel eligibility is coordinated but not atomic. Treat a tagged run as promoted only after both publication jobs and their post-publication checks succeed. If one registry succeeds and the other fails, stop promotion and inspect the partial state. Retry only the missing job or files from the same release tag and verified inputs; never rerun a successful publication blindly or replace an already published immutable Python version.

Promote a release only from the uploaded workflow artifacts after reviewing the attestations. Roll back a Python release by reinstalling the previous verified wheel or sdist while it remains inside that 90-day artifact window. For older rollback targets, preserve the verified wheel/sdist outside GitHub Actions artifact retention or rebuild from the release tag before promotion. Tagged releases also publish the GHCR image with BuildKit SBOM/provenance and GitHub attestations; container rollbacks should pin the previous verified image digest, not a floating tag.

Documentation ownership

  • README.md is the compact repository landing page. It keeps the managed-VPS two-command path first and points to the canonical task pages instead of duplicating their complete procedures.
  • docs/quick-start.md owns the managed VPS first journey, release-image verification, disposable launch, the first file task, and first-run removal.
  • docs/operations.md owns managed/Docker persistence, stop/remove semantics, destructive cleanup, state locations, resource sizing, and update rollback.
  • SECURITY.md owns external-exposure policy; docs/public-direct.md owns the executable public-direct Compose, health, quota, and container-rollback procedure.
  • API.md and ADR-010 own the protocol/client compatibility contract.
  • CONTRIBUTING.md and ADR-007 own release publication and Python/container rollback policy.
  • docs/index.md, docs/architecture.md, docs/threat-model.md, and docs/ADR/* are docs-site pages and stay docs/-canonical.
  • API.md, CHANGELOG.md, CONTRIBUTING.md, and SECURITY.md are root-canonical and are generated into docs/ by tools/sync_docs.py.
  • After editing a root-canonical document, run python tools/sync_docs.py --write and commit both the root file and its generated docs/ mirror.

Product and package compatibility

The current product, distribution, CLI, and public import name is xferry. New public APIs, examples, and type-checking coverage must use the xferry namespace.

The deprecated src package remains installed and supported throughout 2.x for compatibility. Its planned removal boundary is 3.0. Changes in 2.x must keep the compatibility tests passing, while documentation and new APIs guide consumers to migrate public imports and module invocations to xferry before that major release.

Pull request checklist

Reviewers look for (the template in .github/PULL_REQUEST_TEMPLATE.md restates this):

  • [ ] Tests added or updated; pytest green locally.
  • [ ] ruff check xferry src tests passes.
  • [ ] ruff format --check xferry src tests passes.
  • [ ] mypy xferry src passes with no new errors.
  • [ ] CHANGELOG.md updated under [Unreleased] with the appropriate section (Added, Changed, Fixed, Security, Performance, Deprecated, Removed).
  • [ ] Root-canonical docs regenerated with python tools/sync_docs.py --write and verified with python tools/sync_docs.py --check.
  • [ ] Stale contract and semantic docs guards pass with python tools/check_stale_docs.py.
  • [ ] Documentation updated when behavior changes: README.md, API.md, or a new docs/ADR/ entry.
  • [ ] Security impact statement in the PR body (say "none" explicitly if the change is unrelated to auth/TLS/crypto/path handling).

Code conventions

  • Python 3.10-compatible syntax — use X | None, not Optional[X].
  • Type hints on everything that is not a test fixture. mypy --strict is enforced on xferry/ and src/.
  • Paths: always pathlib.Path; compare with Path.resolve().relative_to() for any user-supplied component (see ADR-004).
  • Randomness: use secrets for anything security-adjacent, random for nothing in src/.
  • Logger: logging.getLogger("xferry"). Log what a reader needs to investigate a failure — include the request ID when available.
  • Error responses: JSON body {"error": "...", "status": NNN}.
  • English in user-facing strings (logs, CLI --help, response bodies). Source-code comments may be in any language the team can read.

Adding a new HTTP method

  1. Create or extend the scoped handler mixin in src/handlers/.
  2. Define def handle_<name>(self, request: HTTPRequest) -> HTTPResponse.
  3. Add one CoreMethodSpec in src/features.py; handler registration, CORS, browser-mutation policy, PING discovery, and UI grouping derive from it.
  4. If a new handler mixin is required, add it to HandlerMixin in src/handlers/__init__.py.
  5. Document the method in API.md and, if it has non-trivial semantics, add a short ADR in docs/ADR/.
  6. Tests:
  7. Unit tests in tests/test_handlers/test_<mixin>.py
  8. Integration scenarios in tests/test_handlers/test_handler_integration.py if the method interacts with sockets, auth, or advanced upload routing.

Reporting security issues

Do not open a public issue. See SECURITY.md for the private disclosure process.

Questions

Open a draft PR or issue — happy to discuss design before code.