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
  1. Problems and ideas
  2. Changing the code
  3. Checks
  4. The website in other languages
  5. Labels
  6. Releases
  7. Licence

Problems and ideas

Security problems are reported privately. Use Report a vulnerability, not an issue, so the problem can be fixed before it is public.

Changing the code

Nothing is pushed to main directly. Every change is a pull request from its own branch.

  1. Start from an issue. For anything larger than a small fix, agree on the idea there first.
  2. Create a branch from main, named after what it does:
    BranchFor
    feat/share-link-editingSomething new
    fix/upload-resumeA repair
    docs/backup-guideThe website and guides
    chore/update-actionsTools, builds and dependencies
  3. Make the change, and run the checks below.
  4. Open a pull request. Give it a title that says what changes in plain words, and one label. Write Closes #123 in the description to close the issue when it is merged.
  5. Wait for the checks. The lightweight Checks passed gate must pass before it can be merged.
  6. 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
PartChecks
scripts/check.sh quickAutomatic 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 webThe 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 serverThe server: formatting (cargo fmt --check), cargo test, and cargo clippy with warnings as errors
scripts/check.sh e2eThe 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 siteThe 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.

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:

PageEnglishOther languages
Home pagesite/index.htmlsite/<language>/index.html
Installsite/docs/index.htmlsite/<language>/docs/index.html
All other guidessite/docs/<guide>.htmlsite/<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.

Labels

LabelIn the release notes under
enhancementNew
bugFixed
performanceFaster
accessibilityEasier to use for everyone
documentationGuides and website
dependenciesUpdated dependencies
maintenanceMaintenance

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).

NumberGoes 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.