Contributing
How to report a problem, suggest an idea, or change the code of ThirtyFile.
Everyone taking part follows the code of conduct.
On this page
Problems and ideas
- Open an issue and pick the form that fits: Report a problem, Suggest an idea, Report an accessibility barrier, Improve the guides or Improve the code. The form asks for what is needed to help you.
- Look through the existing issues first. If yours is there, add to it instead of opening a new one.
Changing the code
Nothing is pushed to main directly. Every change is a pull request from its own branch.
- Start from an issue. For anything larger than a small fix, agree on the idea there first.
- Create a branch from
main, named after what it does:Branch For feat/share-link-editingSomething new fix/upload-resumeA repair docs/backup-guideThe website and guides chore/update-actionsTools, builds and dependencies - Make the change, and run the checks below.
- Open a pull request. Give it a title that says what changes in plain words, and one label. Write
Closes #123in the description to close the issue when it is merged. - Wait for the checks. The lightweight Checks passed gate must pass before it can be merged.
- Merge. The pull request becomes one commit on
main, and its branch is deleted.
Checks
Run the full checks locally before opening a pull request, with one script. Releases and manual full verification run these checks too; automatic pull-request checks are lighter.
scripts/check.sh
| Part | Checks |
|---|---|
scripts/check.sh quick | Automatic pull-request checks: frontend formatting, translations, module boundaries, end-to-end test structure, types and lint; Rust formatting; migration numbering and unchanged released migrations; website links. No unit/browser tests, Rust compilation or image build |
scripts/check.sh web | The interface: dependencies as locked, formatting (pnpm format:check), translations (node scripts/check-i18n.mjs), that the Office code in web/src/ooxml imports nothing else from the app (node scripts/check-boundaries.mjs), that the end-to-end tests have no fixed waits and no names without a random part (node scripts/check-e2e.mjs), types, lint and unit tests. With scripts/check.sh web --coverage, as on GitHub, the unit tests also fail when they cover less than the floor in web/vitest.config.ts |
scripts/check.sh server | The server: formatting (cargo fmt --check), cargo test, and cargo clippy with warnings as errors |
scripts/check.sh e2e | The end-to-end test: signs in, uploads and previews files in a real browser (Playwright), against a server built from the repository. The tests run several at a time (4 on GitHub), so each one works in a folder of its own, gives names a random part, and waits for the server's answer after a change rather than a fixed time: the helpers in web/tests/e2e/helpers.ts do this. The first time, get the browser with cd web && pnpm exec playwright install chromium |
scripts/check.sh site | The website: every translated page exists in each language with matching language links, and the links and anchors inside the site lead somewhere (node scripts/check-site.mjs). It needs only Node, and it runs for changes to the website too |
On Windows, run it in Git Bash. During full verification on GitHub, known security problems of the dependencies are checked too (cargo deny and pnpm audit), and the server's tests and the end-to-end test together must cover at least 90% of the server's lines (cargo llvm-cov, the floor in .github/workflows/build.yml).
The code is formatted by tools, and the checks fail when it isn't. Format it before committing:
cd server && cargo fmt
cd web && pnpm format
The server uses rustfmt (its settings are in server/rustfmt.toml), the interface oxfmt (settings in web/.oxfmtrc.json; it formats web/src, web/tests and web/scripts). An editor with a rustfmt or oxfmt extension can format on save with the same settings. A table laid out by hand, with several entries per line, keeps its layout with a // oxfmt-ignore line above it. The dictionaries in web/src/lib/i18n/ aren't formatted: they keep one entry per line.
Commits that only changed the layout of the code are listed in .git-blame-ignore-revs. GitHub's blame view skips them; for git blame to skip them too, run git config blame.ignoreRevsFile .git-blame-ignore-revs once in your clone.
- Code, comments and guides are written in English.
- Text in the interface is written in English in the code, with its translations in
web/src/lib/i18n/: one folder per language, with the same files in each. Traditional Chinese (zh-TW), Simplified Chinese (zh-CN) and Japanese (ja) are all required: new text needs its translation in each of them, following the glossary below.node scripts/check-i18n.mjschecks every language: it fails when a required language misses an entry or keeps one the code no longer uses, and only reports this for the others, with how many placeholders are left. A language is required once it is listed inCOMPLETEin that script; one offered in the language switch must be. To see what a new language still needs, run it with--require <language>. - The texts the server writes itself, the emails, are in
server/src/i18n/texts.rsin English, with one file of translations per language next to it (zh_tw.rs,zh_cn.rs,ja.rs). A text missing from a language's file is sent in English. The server's tests fail when a language listed inCOMPLETEintexts.rs, or offered in the language switch, misses a text, or when a translation's{parameters}differ from the English ones. - When behaviour changes, update the guide that describes it, in
site/docs/. - The database structure of 0.4.0 is fixed, so that data made by a release keeps working. A change to the structure goes into a new migration file,
server/migrations/0002_<name>.sqland up, that upgrades a 0.4.0 database;0001_init.sqland the files of earlier releases are never edited, and the checks fail when they are. - The web app's API types (
web/src/api/types.ts) are written by hand. A server test records the shapes of the main responses inweb/tests/contract/shapes.jsonand fails when one changes: runTHIRTYFILE_UPDATE_SHAPES=1 cargo test api_shapesinserver/, then update the types untilpnpm typecheckpasses. - Logic that changes people's files (formulas, saving workbooks, text encodings) gets unit tests in
web/tests/unit/.
Every pull request runs the lightweight checks above. Full tests and image builds run before releases and pre-releases, or by hand through Actions › Build › Run workflow; manual verification publishes nothing. Both image architectures are started with compose.yaml on data made by the latest release before publication. CodeQL scans main after merges, every week and before each release. Merging into main publishes nothing, not even the website: the guides are published with each release, so they describe what people can install. A fix to the guides that is true of the released version too can be published by hand: Actions › Website › Run workflow with main as the branch or tag to publish.
The website in other languages
The homepage and all 16 guides are available in English, Traditional Chinese, Simplified Chinese and Japanese. Each language has the same 17 pages:
| Page | English | Other languages |
|---|---|---|
| Home page | site/index.html | site/<language>/index.html |
| Install | site/docs/index.html | site/<language>/docs/index.html |
| All other guides | site/docs/<guide>.html | site/<language>/docs/<guide>.html |
Guide menus, related links, previous/next links and section lists keep the reader in the chosen language. Switching languages stays on the same guide and, with JavaScript, the current section. The README is translated too: README.md, with README.zh-TW.md, README.zh-CN.md and README.ja.md next to it, each linking to the website in its language.
- Changing a translated page means changing it in every language, in the same pull request. If you can't write one of the languages, say so in the pull request, and a maintainer will find someone who can.
- Each version has
<html lang>, its own title and description, and a<link rel="alternate" hreflang>to every version, itself included. The language switch, guide menu and section list are written into every page, so they work without JavaScript. Keep all section IDs and command/configuration examples identical across languages. - The languages and all pages are listed in
LANGUAGESandTRANSLATEDat the top ofsite/assets/site.js, which also holds the menu and footer translations. After changing that navigation or guide headings, runnode scripts/update-site-navigation.mjsto refresh the static menus and section lists.scripts/check.sh sitefails on missing translations, wrong metadata or language switches, missing sections, changed code blocks, broken links and unintended links to another language. Add every new page in every supported language in the same pull request. Runnode --test scripts/check-site.test.mjsto test the checker; with the frontend's Playwright dependency and Chromium installed,node scripts/check-site-browser.mjs <scratch-directory>checks the rendered pages, language and section switches, mobile layout and navigation without JavaScript, and saves screenshots outside the website. - The words follow the glossary (
web/src/lib/i18n/glossary.md), which the interface's translations use too. Simplified Chinese uses the words of mainland China rather than a character conversion of the Traditional Chinese, and Japanese uses です・ます in sentences and the terms of Windows Explorer and Finder. - The home page's main screenshot is taken in each language, from a local instance with sample data, 1280 × 720 at 2x, saved as WebP (
site/assets/files-light-<language>.webpandfiles-dark-<language>.webp). The other screenshots stay in English.
Labels
| Label | In the release notes under |
|---|---|
enhancement | New |
bug | Fixed |
performance | Faster |
accessibility | Easier to use for everyone |
documentation | Guides and website |
dependencies | Updated dependencies |
maintenance | Maintenance |
Releases
Release images are published to both ghcr.io/thirtyfile/thirtyfile and docker.io/thirtyfile/thirtyfile. Before the first publication to Docker Hub, add an Actions variable DOCKERHUB_USERNAME and an Actions secret DOCKERHUB_TOKEN under Settings › Secrets and variables › Actions. Use a Docker Hub access token with read/write access to that repository. The release checks the credentials before scanning and building; it copies the tested image by digest and verifies every Docker Hub tag before publishing the GitHub release.
Merged changes collect on main until a maintainer decides to release them. A release is started by hand: on GitHub, open Actions › Release › Run workflow, keep main selected and enter the new version number, such as 1.2.3, or 1.2.3-rc.1 for a pre-release. Or from the command line:
gh workflow run release.yml -f version=1.2.3
The workflow scans what is on main with CodeQL, tests it, and builds the image for amd64 and arm64 and starts it. Only once all of that has passed is the image pushed, still without a tag. Then, in this order: the server programs for amd64 and arm64 are taken from the image, and a draft release is made with them, their checksums, the licence notices and compose.yaml, set to the release line (:1.2 before 1.0.0, :1 from then on); the image is tagged 1.2.3, then 1.2, 1 (from 1.0.0 on) and latest, without being built again; the release is published, which creates the tag v1.2.3, with notes put together from the merged pull requests since the last release; and the website is published from that tag. If the scan finds an open alert of high or critical severity, or a test or the image fails, nothing is pushed, tagged or released. Fix the alert, or dismiss it under Security › Code scanning when it doesn't apply, and run the release again. If a later step fails, run the release again with the same number: it publishes the image already tagged 1.2.3 instead of building another. The number must be newer than the last release. Tags starting with v can't be moved or deleted.
A version with a -, such as 1.2.3-rc.1, is a pre-release: its image is only tagged 1.2.3-rc.1, and GitHub marks the release as a pre-release, so nobody gets it without asking for it; the website isn't published for it. When the pre-release is good and nothing has been merged since, release 1.2.3 from the same commit: the workflow tags the pre-release's image instead of building a new one, so people get exactly what was tried (its version label still names the pre-release).
| Number | Goes up when |
|---|---|
First (2.0.0) | Something people rely on changes or is removed |
Second (1.3.0) | Something new is added |
Third (1.2.4) | Only repairs |
To copy an existing release, open Actions › Mirror a release to Docker Hub › Run workflow, select main, and enter its published version, such as 0.6.0. The workflow verifies the Git tag, image revision and signed GHCR provenance before copying the image. It only advances a release-line, major or latest tag when GHCR currently points that tag to the same image; mirroring an older release cannot replace a newer one. An existing exact-version tag must match the source digest or the workflow stops. Retrying a matching copy is safe, and no image is rebuilt.
Licence
ThirtyFile is licensed under the Apache License 2.0. What you contribute is licensed under the same terms.
Dependencies must have a permissive licence: the Rust ones are checked against the list in server/deny.toml, which server/about.toml repeats, and the npm packages against the same list (plus the font licence OFL-1.1) in web/scripts/third-party-notices.mjs. The tests check both. The image build collects the licence notices of every crate and npm package, and of musl, the certificate authorities and the time zone database, into THIRD-PARTY-NOTICES, and fails when a licence isn't on the list.