← Ataski · Trust & security · DPA · Subprocessors

This is the canonical English version. Native translations for DE / ES / FR / PT / RU are pending counsel sign-off.

Approved 2026-10-08

Change Management Policy

Owner: the founder. Approved: 2026-10-08. Review: annually.

1. The invariant

local main == origin/main == production. Always. Every rule below exists to protect that sentence.

It is written this bluntly because it was broken once. On 2026-05-21 the local main and origin/main had diverged into two different codebases, because commits had landed directly on a local main that was never pulled and deploys had been done by copying individual files to the server. The result was a production system that matched no single git ref — nothing could be reasoned about, reproduced or rolled back.

2. How a change reaches production

  1. Sync first. git checkout main && git pull origin main before branching. A stale main is how divergence begins.
  2. Branch. feature/*, fix/* or chore/*. Never commit to main — not locally, not in a worktree.
  3. Commit. Conventional Commits (feat:, fix:, chore:, docs:, refactor:, test:). The commit hook (scripts/git-hooks/pre-commit) runs .pre-commit-config.yaml on the staged files: ruff, gitleaks, detect-private-key and the file-hygiene checks. mypy is left to the pre-push gate — minutes on a fresh worktree, too slow for every commit.
  4. Open a PR with the What / Why / How / Testing / Risk description.
  5. The pre-push gate must be green before a push leaves the machine, and on the PR head before merge. scripts/check.py, run by the git pre-push hook, refuses the push unless every stage passes: ruff (including the S security ruleset), mypy --strict, gitleaks over the branch's commits and over the whole history, trivy fs over the lockfile and Dockerfile (CRITICAL blocks), the pytest suite, the Redis-backed counter checks, the Schemathesis API fuzz, and the live Postgres stage that upgrades to head, downgrades, re-upgrades and then asserts that RLS actually bites as the runtime role (ADR-0119). Both hooks are installed once per clone with scripts/install_git_hooks.sh.
  6. Merge into origin/main.
  7. Re-sync the local main immediately, so it cannot drift.
  8. Deploy the whole ref from origin/main with uv run python infrastructure/deploy.py --apply. The script builds a deterministic tarball from tracked files only, rsyncs with --delete so removed files actually disappear, refuses unless the server .env exists with mode 0600 or tighter (it never writes one), brings the compose stack up, and polls /health until it answers 200.

3. What needs explicit approval before it is built

Per CLAUDE.md, the founder decides — not the implementer — on: a new external service or vendor, any change to the architectural principles, a major-version dependency upgrade, a schema change requiring migration, anything costing more than $20/month, anything affecting the security posture, pricing, and any major customer-facing copy or UX change. Everything else (library choice inside the stack, code structure, tests, refactors, bug fixes with a clear root cause, logging, semver-compatible dependency updates) is decided autonomously.

Architectural decisions are recorded as ADRs in docs/adrs/ before the change lands, not after it ships.

4. Emergency changes

Copying a file to the server with scp is forbidden as a routine deploy. It is the tool that produced the divergence in §1. As an emergency hotfix it is permitted only under these conditions, all of them:

5. Rollback

Redeploy the previous origin/main ref. Because the deploy is a whole-tree rsync of a git archive, "the previous version" is a commit, not a reconstruction. Migrations are the exception that needs thought: a rollback across a migration requires the corresponding downgrade, which the pre-push gate has already exercised for the recent chain.

6. Evidence this produces

Every merged PR is a dated change record with its description — reviewed by the gate and by its author, not by a second person; the git history is the change log; deploy.py output is the deployment record. For the auditor, the export is a list of merged PRs for the period plus the output of a full gate run — see docs/security/evidence-checklist.md §3.

7. Known gaps

Branch protection on origin/main is discipline, not a GitHub setting — and today it cannot be one: GitHub branch protection and rulesets are not available for a private repository on the free plan. A direct push to main is technically possible; what stops it is §2 rule 2, and what stops it reaching production is the deploy's ref guard. Turning "require a pull request" into a setting needs a paid GitHub plan (about $4 per user per month) — a money decision, recorded in docs/security/soc2-readiness.md CC8.1.

There are no server-side required checks. The commit hook and the pre-push gate run on the machine the change was written on, so there is no independent witness: nothing on GitHub shows that they ran, and git commit --no-verify / git push --no-verify skip them. --no-verify is an emergency hotfix tool only, under the same conditions as §4 (ADR-0119).