Contribute to the project#
Contributions and issues are most welcome! All issues and pull requests are handled through GitHub. Also, please check for any existing issues before filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don’t spend your time coding something that might not fit the scope of the project.
To judge the scope, read the
Goals in the README
(tiny, one service, no login code, not multi-user) and the
design. For a UI change see the
design system. Record a
significant decision as an ADR: copy
docs/explanations/decisions/COPYME
to the next number (see
ADR 1).
Issues#
Use the bug report or issue template. Both ask for Acceptance Criteria, the specific conditions that say the issue is fixed. Open-ended ideas and design questions go in an issue too (the issue template covers design discussions).
Set up and check#
You need uv. uv sync installs the project and its
dev dependencies (the dev group) into a virtual environment. Python 3.11 or
newer is supported, CI covers 3.11 to 3.14, and .python-version pins 3.11 for
local work.
uv sync
uv run pre-commit install # run the hooks on every commit
uv run tox -p # pre-commit, type-checking, tests and docs in parallel
or one at a time:
uv run tox -e pre-commit # ruff lint and format, uv.lock sync, gitleaks, file checks
uv run tox -e type-checking # pyright on src and tests
uv run tox -e tests # pytest with coverage
uv run tox -e docs # build the docs into build/html
uv run tox -e docs-autobuild # rebuild the docs on change
The docs build fails on any warning and needs
graphviz installed. pytest also runs the doctests in
the docstrings under src/ (the docs are Markdown, which it does not collect),
and treats warnings as errors.
Writing the docs#
The docs are MyST Markdown under docs/, in the Diataxis folders tutorials/,
how-to/, explanations/ and reference/. A new file in tutorials/,
how-to/ or explanations/ is picked up by the glob in that section’s
toctree. docs/reference.md lists its pages one by one, so add a line there
for a new reference page.
Use relative links and
{ref}labels insidedocs/. InREADME.mdand this file use absolute GitHub URLs, becausedocs/how-to/contribute.mdonly includes this file and a relative link would resolve from there.The build fails on any warning.
conf.pysetsnitpicky, so a reference that does not resolve is a warning too.The build needs network access (it fetches the version switcher and the Python intersphinx inventory) and a git checkout (a development version takes its name from
git branch --show-current).docs-autobuildandscrap serveboth default to port 8000. Pass another port to one of them, for exampleuv run tox -e docs-autobuild -- --port 8001.A weekly workflow (Wednesdays, also runnable by hand from the Actions tab) checks that the links in the docs still resolve. To run the same check locally:
uv run tox -e docs -- -b linkcheck.
Run it from a checkout#
uv run scrap serve --data-dir ./data # port 8000 by default
curl http://127.0.0.1:8000/healthz
uv run scrap --help lists the other commands. The UI is the PWA built from
web/, so build it first (next section); without it scrap serve still serves
the API. See the
API reference and
capture from your phone.
Capture and search work without Claude. The Claude features (photo ingest and
page enrichment) need SCRAP_INGEST=on and ANTHROPIC_API_KEY in the
environment. The tests do not call Claude.
Working on the PWA#
The PWA is TypeScript in web/, built with Node 22 into src/scrap/static/.
The devcontainer does not install Node, so install it yourself for this work.
npm --prefix web ci
npm --prefix web run build # writes src/scrap/static/, which scrap serve picks up
npm --prefix web run dev # Vite dev server; run scrap serve beside it, it proxies to port 8000
npm --prefix web run check # Biome and tsc
npm --prefix web run fix # Biome, applying fixes
npm --prefix web test # vitest unit tests
npm --prefix web run size # after a build: the app shell stays within 16.5 kB gzipped
npm --prefix web run e2e # Playwright against a real scrap serve
Playwright needs a browser first: npx playwright install chromium (from web/).
The e2e tests serve on port 8781 with a throwaway data folder; set
SCRAP_E2E_PORT or SCRAP_E2E_DATA to change either. Publishing is on in them: the public
site’s receiver (scrap-public/receiver.py) runs on that port + 100 with a throwaway site
folder and token, and web/e2e/site.py serves the site as nginx would on the port + 200
(web/e2e/ports.ts); the folder and token are removed after the run. Runs side by side need
bases at least 1 apart and within 100 of each other (8801, 8802, …), so no port is shared.
The specs share one server and one data folder, so they run on one worker. CI splits them across
six runners (--shard=N/6), each with its own server and data. To go faster locally, run shards
side by side, each on its own port: SCRAP_E2E_PORT=8791 npm run e2e -- --shard=1/6, and so on.
Tests and coverage#
New behaviour needs a test: pytest under tests/ (conftest.py provides the
client and note fixtures), vitest under web/tests/ and Playwright under
web/e2e/.
uv run tox -e tests prints a coverage report and writes cov.xml. CI uploads
it to Codecov, which reports on the pull request. Nothing fails the build on a
coverage drop, so please make sure coverage remains the same or is improved by a
pull request.
Deployment scripts and the Helm chart#
The scripts in deploy/ are covered by tests/test_deploy.py, which runs under
pytest. Its tests put a fake podman (or systemctl) in a temporary folder that records its
arguments (the scripts call /usr/bin/podman, so the tests point PODMAN at the
fake; it is a test hook, not a setting), so none of them needs podman or root. When you change a script, add
a test of that kind instead of trying it on a live install.
./deploy/test-retention.sh runs the scrap-backup pruning checks on their
own, also without podman or root, and prints ok or FAIL for each case. It
checks that only the newest N backups are kept, that paths with spaces are safe,
that symlinks and other directories are left alone, that a bad KEEP value
(0, 00, empty, abc, -1, 2x) is refused, and that a second run in the
same second stops before it calls podman. tests/test_deploy.py runs it as
test_backup_retention, and also tests the scrap-gc wrapper. Extend these
when you change scrap-backup or scrap-gc. ./deploy/test-check-env.sh does the
same for deploy/check-env, which install.sh and smoke.sh use to refuse env
files that systemd and the shell would read differently.
After a change to charts/, run ./charts/test-chart.sh (needs helm and
kubeconform on your PATH). For every charts/scrap/ci/*-values.yaml it:
lints with
--strict, renders the chart (thelong-namescase uses a 53-character release name) and validates the output withkubeconform -strictagainst Kubernetes 1.30.0 and 1.36.5. SetK8S_VERSIONSto change that, for exampleK8S_VERSIONS="1.30.0" ./charts/test-chart.sh.fails if a Secret or ConfigMap is rendered, if
ANTHROPIC_API_KEY,SCRAP_NOTIFY_URLorSCRAP_MCP_TOKENappears as a plain value, if the Deployment is not one replica withRecreate, or if a name or label exceeds the Kubernetes limits.
It also checks that the chart refuses values such as ingest.enabled without
existingSecret, ingest.apiKey, an unknown top-level key, backup.keep of
0 or -1, and a non-numeric or negative budget. Finally, every SCRAP_* or
ANTHROPIC_* variable the chart sets must have a row in the Settings table in
docs/reference/api.md (a line starting | `VAR` |) or be read by
deploy/scrap-backup, so document a new variable there.
To add a chart option, update values.yaml and values.schema.json (which
refuses unknown keys) and add a ci/<name>-values.yaml that sets it. A release
tag sets the chart’s version and appVersion when CI publishes it, so
charts/scrap/Chart.yaml keeps the placeholder 0.0.0. CI runs the script
under Helm 3 and Helm 4.
Pull requests#
Fill in the pull request template:
Start with
Fixes #ISSUE, linking the issue.Give the reviewer steps to test it.
Write a title that would make sense to a user in a list of release notes.
Claude follows the scrap-pr skill (.claude/skills/scrap-pr/) for any pull request here,
alongside the owner’s general pr-cycle user skill; see docs/how-to/work-with-claude.md.
CI must pass:
lint: the pre-commit hooks (ruff lint and format,
uv.lockin sync, gitleaks, file checks) and pyright.test: pytest on Python 3.11, 3.12, 3.13 and 3.14.
deploy: shellcheck on the
deploy/scripts, and thedeploy/test-*.shtests.web: Biome and tsc, vitest, the PWA build and size budget, and Playwright.
container, docs and dist: the container image, the docs and the package must all build. The image is published on release tags.
Chart:
charts/test-chart.shunder Helm 3 and Helm 4.
Making a release#
Releases are cut by pushing a tag. The tag is the version, with no v prefix
(for example 0.2.0), and the package version is taken from it by
setuptools_scm, so there is no version to edit in pyproject.toml
(src/scrap/_version.py is generated).
On
main, tag the merge commit and push the tag (the tag, not a branch, starts the release). There is no need to wait formain’s CI first: the tag’s CI runs the same checks, and no image is published unless they pass (the chart and docs still are). The chart version needs no edit: CI sets it from the tag.git tag X.Y.Z git push origin X.Y.Z
The CI workflow then runs on the tag. The GitHub Release is created once the
dist, test, deploy, docs and web jobs have passed. It has generated release
notes with the sdist, the wheel, docs.zip and install.sh attached. In parallel:
The image is built natively for amd64 and arm64, each on a runner of that architecture, while the tests run, and each is pushed by digest only. Once the
lint,test,deployandwebjobs have passed, thecontainer-publishjob tags them together as one multi-arch image,ghcr.io/gilesknap/scrap:X.Y.Zandlatest. The image and the chart are all a deploy needs; the docs and the GitHub Release may finish later.The public site’s image,
ghcr.io/gilesknap/scrap-public(nginx and the upload receiver fromscrap-public/, for the chart’spublicsection), is built, tested withscrap-public/test-image.shand published the same way, by thecontainer-public-publishjob behind the same gates. Each publish job also waits for the other image’s build and checks, so a tag publishes both images or neither. The first time it is published, make thescrap-publicpackage public in the GitHub package settings.The docs are published to GitHub Pages as a new version in the version switcher.
Things to know:
A tag containing
a,borrc(such as0.2.0rc1) is marked as a pre-release on GitHub, and its image is not taggedlatest(only a tag without-,a,borrcmoves it).The release also has
install.shattached:commands/install.shwith itsdefault_refset to the tag, so the README’s one-liner (releases/latest/download/install.sh) installs the commands of the latest release, not ofmain. Nothing to edit per release.The Chart workflow is separate. On a tag it packages the chart with
versionandappVersiontaken from the tag and pushes it tooci://ghcr.io/gilesknap/charts/scrap. A pre-release tagX.Y.ZrcN(oraN,bN) is published as chart versionX.Y.Z-rc.N, withappVersionthe tag as written so the image resolves. A tag in any other form fails the publish only; the image and the release are still published. The first time, make thecharts/scrappackage public in the GitHub package settings.The release job does not wait for the
container-publishjob, so check that the image was pushed.Nothing is published to PyPI.
Developer Information#
It is recommended that developers use a vscode devcontainer,
with podman or docker. The configuration in .devcontainer/ builds the developer
stage of the Dockerfile (Ubuntu with graphviz), then runs uv sync and installs the
pre-commit hooks. It does not include Node.
The container mounts the parent directory of your checkout at /workspaces. It
runs with --net=host, so scrap serve on port 8000 inside the container is
localhost:8000 on the host, and clashes with anything already using that port
(pass --port to scrap serve to avoid it). The virtual environment is not
.venv: it lives in the devcontainer-shared-cache volume (mounted at
/cache, along with the uv and pre-commit caches) under a name taken from the
workspace path. It is already on PATH and set as the VS Code interpreter, so
uv run works as normal. To start clean, remove that volume.
Renovate keeps uv.lock current and automerges its lock-file maintenance pull
requests once CI passes. It is told to leave the workflow action versions and
the Ubuntu and devcontainer base images alone, because the Copier template
manages them (copier update), so do not expect update pull requests for those.
After you change dependencies in pyproject.toml, run uv sync and commit
uv.lock; the pre-commit uv-sync hook checks it.
This project was created using the Diamond Light Source Copier Template for Python projects
(ADR 2).
For generic tasks the template’s
How-to guides
may help, but the commands above are the ones this project uses. Update that link when
copier update moves the template version (_commit in .copier-answers.yml).