Updating from upstream
Your app is a copy of the template. The template keeps getting fixes and
features, and you take them by merging its main branch into yours. Git
calls the repository you copied from upstream.
Start with a clean working tree (git status shows nothing to commit)
and do everything on a branch:
git checkout -b update-template
1. Add the template as a remote, once
git remote add upstream <template repository URL>
git fetch upstream
git remote -v now lists origin (your repository) and upstream.
2. If git finds no shared history, record your starting point, once
Check with git merge-base HEAD upstream/main. If it prints a commit,
skip to step 3. If it prints nothing, step 3's git diff would fail with
fatal: HEAD...upstream/main: no merge base and step 4's merge with
fatal: refusing to merge unrelated histories. Your repository was
created from a copy of the files rather than a clone, so git does not
know which template commit you started from. Find it by looking for the
upstream commit whose files match your first commit:
first=$(git rev-list --max-parents=0 HEAD | head -1)
tree=$(git rev-parse "$first^{tree}")
for c in $(git rev-list upstream/main); do
[ "$(git rev-parse "$c^{tree}")" = "$tree" ] && echo "$c"
done
Record it. This changes no files:
git merge -s ours --allow-unrelated-histories -m "Record the template commit this repo started from" <commit>
Every step below then works as written. If the loop prints nothing, your
first commit already differed from every template commit. Skip the
record, read upstream's whole changelog with
git show upstream/main:CHANGELOG.md instead of step 3's diff, and in
step 4 run git merge --allow-unrelated-histories upstream/main. Every
file both sides have, with different contents, then conflicts and
shows in git status as both added; resolve each as in step 5.
Later updates work as written.
3. Read the changelog before you merge
git fetch upstream
git diff HEAD...upstream/main -- CHANGELOG.md
The three dots show only what upstream added since your last merge. Read every new entry, and start with each release's Breaking / action required list: those are the changes that merge and compile cleanly, then behave differently, and the things you must do by hand. Note them down; step 7 needs them.
4. Merge
git merge upstream/main
Use merge. A rebase rewrites your own commits, which breaks
everyone who already pulled them. A merge adds one commit, and the next
git merge upstream/main starts from it.
5. Resolve conflicts
git status lists conflicted files as both modified.
Source files first (
*.ts,*.tsx). Open each, keep both sides' intent, and delete the<<<<<<<,=======and>>>>>>>lines. A list that both sides added to, such asDELETION_POLICYinbackend/src/users/deletion-policy.tsorPOLICYinbackend/src/common/route-auth.spec.ts, usually needs both entries.package.json, thenpackage-lock.json, in each half that changed. Resolvepackage.jsonby hand, keeping both sides' entries. Leavepackage-lock.jsonas git merged it, conflict markers included, and runnpm installin that half:cd backend && npm installnpm resolves conflict markers in the lock file itself, keeps the versions both sides already locked, and adds whatever
package.jsonnow asks for. Do not replace the lock with upstream's copy: that drops any version you pinned in the lock alone, such as a security bump of a nested dependency. Do this before item 3, because the dump there loads the merged code and fails if a new dependency is not installed.openapi.jsonandfrontend/types/api.d.ts: do not edit them. Once no source file has conflict markers left and dependencies are installed, regenerate both, frombackend/:npx ts-node -r tsconfig-paths/register scripts/dump-openapi.ts cp openapi.runtime.json ../openapi.json cd ../frontend && npm run generate-typesThe dump reads the code and ignores the conflicted
openapi.json, so the result matches the merged code.
Then git add the resolved files and git commit to finish the merge.
6. Migrations with the same number
Migrations in backend/migrations/ are numbered: 001_initial.ts, and so
on. If you added 016_notes.ts and upstream later added
016_audit_log.ts, both files arrive side by side. Leave both as they are.
What Knex does, tested on 2026-10-25:
- On your database, where
016_notes.tsalready ran,npm run migrateruns016_audit_log.tsand printsBatch N run: 1 migrations, where N depends on how many batches that database has run. Knex runs every file it has not recorded in itsknex_migrationstable, whatever its number, and reports no error. - On a fresh database, both run in file-name order:
016_audit_log.tsbefore016_notes.ts. So the two must not depend on each other, which is true unless one changes a table the other creates. npm run migrate:rollbackundoes the last batch only, here016_audit_log.ts.- Never rename a migration that has already run. After renaming
016_notes.tsto017_notes.ts,npm run migratefails withError: The migration directory is corrupt, the following files are missing: 016_notes.tson every database that ran the old name. Renaming it back fixes it.
Your next migration takes the number after the highest one on disk.
7. Install, migrate, verify
From the repository root:
cd backend && npm ci && npm run migrate && cd ..
cd frontend && npm ci && cd ..
./verify.sh
It must end verify: all green (integration specs ran). A guard spec
failing here usually means one side added something the other's lists do
not have yet; Testing says what each one
wants. Do the by-hand items from the changelog now, then commit and merge
the branch into your main.
8. Deploy
Follow Deployment: read the changelog entries again for anything that touches production (a new env var, a webhook event to subscribe to), then build, migrate and start as in its step 4. Run the migration before the new code takes traffic.
What the test run found
On 2026-10-25, a fake upstream and a fake fork were made from the
template. The fork added 016_notes.ts, a DELETION_POLICY entry, a
dependency and a changed @ApiOperation summary, and ran its migration.
Upstream added 016_audit_log.ts, its own policy entry, another
dependency, another summary and a changelog entry.
git merge upstream/mainconflicted inapp.controller.ts,deletion-policy.ts,openapi.jsonandtypes/api.d.ts. Bothpackage-lock.jsonchanges merged without a conflict.- Regenerating after resolving the two source files cleared both generated files.
npm run migrateon the fork's database ran only016_audit_log.ts.- In a second test, both sides pinned
msto different exact versions, sopackage.jsonandpackage-lock.jsonboth conflicted. After resolvingpackage.json,npm installremoved the lock's conflict markers andnpm cisucceeded. In a third, the fork had bumped a nested dependency in the lock alone (@babel/helper-string-parser7.29.7 to 7.29.11) while upstream added a dependency: the merged lock plusnpm installkept both, and upstream's lock would have reverted the bump. - verify then failed twice on the test's own mistakes: knip's
Unused dependenciesfor the two dependencies nothing imported, anddeletion-policy.spec.tsfor policy entries withoutcolumns. Once those were fixed, verify endedverify: all green (integration specs ran).
Related
- Changelog
- Deployment
- Troubleshooting
- Home - Docs index