Spells out the two-channel CI trigger (dev branch for test builds, v* tags for releases) as an actual step-by-step process: keeping dev in sync with main/master, bumping package.json, tagging, and the required Gitea repo secrets/variables.
4.3 KiB
Git workflow
This project uses a lightweight trunk-based workflow. It's a solo/small-team
project, so the goal is just enough process to keep main deployable and the
history readable — not ceremony for its own sake.
Branches
main— always deployable. Nothing is committed tomaindirectly except by merging a branch through the steps below.feat/<short-name>— new functionality (e.g.feat/gif-picker).fix/<short-name>— bug fixes (e.g.fix/expired-proposal-redirect).refactor/<short-name>— internal changes with no user-facing behavior change (e.g.refactor/ui-ux-shadcn).chore/<short-name>— tooling, deps, config.
Branch names are kebab-case and describe the change, not the ticket number (there's no issue tracker yet).
Commits
Follow Conventional Commits:
<type>(<optional scope>): <summary, imperative mood, no trailing period>
<optional body: why, not what>
Types: feat, fix, refactor, style, docs, chore, test.
- Keep commits small and focused — one logical change per commit.
- Write the summary in the imperative ("add", not "added"/"adds").
- Explain why in the body when the reason isn't obvious from the diff.
Merging
- Rebase your branch on
mainbefore merging (git fetch && git rebase origin/main) to keep history linear — don't mergemaininto a feature branch. - Squash-merge feature/fix/chore branches into
mainso each lands as one commit. Largerrefactor/*branches with independently meaningful commits may use a regular merge instead of squashing, if keeping the granular history is genuinely useful for a future bisect. - Delete the branch after it's merged.
- Never force-push
main. Force-pushing a feature branch you own (after a rebase) is fine; never force-push a branch someone else might be using.
Releases & Docker images
CI (.gitea/workflows/docker-publish.yml) never builds an image from a
plain commit or merge to main/master — only from two deliberate
triggers, each producing tags on both the Gitea registry and GHCR:
| Trigger | Produces | Purpose |
|---|---|---|
push to dev |
:dev |
test a build before releasing |
push a v* tag |
:latest + :vX.Y.Z |
ship an actual release |
Testing a build (dev)
dev is a disposable pointer, not a branch with history of its own — it
should always match main/master exactly. Bring it up to date and push
whenever you want a fresh test image:
git checkout dev
git merge main --ff-only
git push origin dev
If dev has drifted and won't fast-forward (e.g. after a rebase, or a
commit landed only on dev by mistake), reset it instead of merging —
dev has no unique history worth preserving:
git checkout dev
git reset --hard main
git push origin dev --force-with-lease
Shipping a release (tag)
- Bump the version in
package.jsononmain/master(a plain commit — builds nothing on its own):# edit package.json: "version": "X.Y.Z" git add package.json git commit -m "chore: bump version to X.Y.Z" git push origin main - Tag that commit and push the tag — this is what actually triggers the
release build:
git tag vX.Y.Z git push origin vX.Y.Z - Never move or re-push an existing tag once it's out (it may already be pulled/deployed). If you need to fix something after tagging, bump to the next patch version and tag again instead.
Use v<major>.<minor>.<patch> (semver) for tags.
Required repo secrets/variables (Gitea → Settings → Actions)
vars.REGISTRY_HOST— the Gitea instance hostname (no protocol), used to tag/push to the built-in Gitea container registry.secrets.REGISTRY_TOKEN— a Gitea PAT withwrite:packagescope.secrets.GHCR_TOKEN— a GitHub PAT withwrite:packagesscope, used to also push toghcr.io.
What not to commit
- Anything in
.gitignore(.env,/data,.next,node_modules, prisma migration SQL underprisma/migrations/*/migration.sql). - Generated/build artifacts.
- Secrets of any kind, even in comments or examples — use
.env.examplefor documenting required variables.