# Versioning and releases ## Git hooks and versioning `scripts/pre-commit` bumps the patch number of `version` in `plugin.json` on every commit (`1.0.0` → `1.0.1`) and stages it into that same commit. `scripts/post-commit` re-stages `plugin.json` afterwards, because commits of a file selection (`git commit -- `, which some IDEs use) are built from a temporary index and would otherwise leave it looking modified. **The hooks enable themselves.** Git never activates hooks from repository content, so a fresh clone, or a repo created from this template, starts without them. The first run of `tools/bundle.py` fixes that by running `git config core.hooksPath scripts`, and every workflow runs it: directly, or through `scripts/deploy.sh` and `scripts/watch.sh`. That includes `--check`, so after cloning run ```bash python3 tools/bundle.py --check ``` once, before your first commit. A commit made before any build runs without the hooks. `bundle.py` leaves `core.hooksPath` alone when it is already set (so pointing it elsewhere on purpose sticks), skips the step in CI (whenever `CI` is set) and outside a git work tree, and never fails a build over it. To turn the hooks off for a clone, run `git config core.hooksPath .git/hooks`; `git config --unset core.hooksPath` only lasts until the next build. The bump is skipped when: - the commit message contains `[no bump]`, `[skip bump]`, `[no version bump]` or `[skip version]` (case-insensitive; `nobump`, `no-bump` and `no_bump` also work), - `NO_BUMP=1` (or `GIT_NO_BUMP=1`) is set, e.g. `NO_BUMP=1 git commit`, which is the only way for commits written in an editor, since their message doesn't exist yet when the hook runs, - `version` in `plugin.json` already differs from `HEAD` (you bumped it by hand, e.g. for a minor or major release), - a merge, rebase, cherry-pick or revert is in progress. Because every version change publishes a release (see [Publishing a release](#publishing-a-release)), a normal commit + push to `main` ships a new patch release. Use a stop word for commits that should not. ## Publishing a release `.gitea/workflows/release.yml` builds, verifies and publishes the `.plugin` to the repo's Releases tab on every push to `main`. `dist/` is gitignored, so the artifact is always built by CI rather than committed. Nothing in the workflow is plugin-specific — it reads the id and version straight out of `plugin.json`, so it keeps working after you rename the plugin. The `version` field in `plugin.json` is the single source of truth: | Push to `main` with… | Result | | --- | --- | | a new `version` | creates tag `v` and a new release | | the same `version` | rebuilds the existing release, replacing its attachment | So every push leaves a downloadable build attached to the current version, and bumping `version` is what cuts a new release. With the git hooks enabled every commit bumps it, so every push of a normal commit cuts a new patch release. Pushing a `v*` tag by hand still works and uses that tag verbatim, as does a manual run (Actions → Publish plugin → Run workflow). Every path runs `--check`, builds with `--release`, and gates the upload on `verify_bundle.py`, so a bundle that fails verification is never published. A rebuild replaces the attachment rather than piling up duplicates, and refreshes the release notes to name the commit it came from. The tag itself is never moved: it keeps pointing at the commit that first introduced that version. Authentication uses `secrets.API_TOKEN`, an organisation-level Actions secret holding a user token with the `write:repository` scope (which covers releases). Set it once under **Org → Settings → Actions → Secrets** and every repo in the organisation inherits it; a repo-level secret of the same name takes precedence if one repo ever needs different credentials. The workflow passes it to `release.py` as `RELEASE_TOKEN`, **not** `GITEA_TOKEN`. The Actions runner injects its own `GITEA_TOKEN` into every step, and that value overrides a step-level `env:` entry of the same name, so the org secret would be silently ignored. If the org secret does not reach a repository the publish still works: `release.py` tries `RELEASE_TOKEN` first and then falls back to the runner's `GITEA_TOKEN`, which can publish releases in its own repository, and prints which one it used. An organisation secret is only exposed to a repository that has Actions enabled, so check **Repo → Settings → Actions** if a repo unexpectedly falls back. If you would rather not manage a token at all, that injected `GITEA_TOKEN` can create releases in its own repo: pass `--token-env GITEA_TOKEN` and drop the org secret. It is scoped to the single job and expires with it, so it is the safer default; `API_TOKEN` is what you want when publishing needs to reach beyond this repo.