# Release / Version Process SOP

Operator standard operating procedure for cutting a versioned release of
Gitea-Tools: version bump, checks, merge, tag, and cleanup.

> **Scope.** This is the **human/operator** SOP. It is deliberately distinct
> from [`release-workflows.md`](release-workflows.md), which describes the
> **future `release-mcp` orchestrator** boundary (a coordination concept), not
> the day-to-day tagging process. When they disagree, this document governs how
> a release is actually cut today.

---

## 1. Branch flow

The repo is **`master`-based**. Releases are cut from `master`; there is no
separate `dev`/`release` branch unless and until that is explicitly introduced
and this SOP is updated to match. All work lands on `master` via reviewed PRs
from short-lived, issue-linked branches (e.g. `docs/issue-68-...`).

## 2. Where "the version" lives

There is **no `VERSION` file and no `CHANGELOG` file** in the repo today. The
released version is expressed **only as an annotated git tag** of the form
`vMAJOR.MINOR.PATCH` (existing tags: `v1.0.0`, `v1.0.1`). Release notes are
carried as the **annotated tag's message** (via `--notes-file`), not a tracked
changelog.

> Do **not** confuse this with `SUPPORTED_VERSION` in `gitea_config.py` — that is
> the **config-schema** version, unrelated to the application release version.

If a `VERSION`/`CHANGELOG` file is added later, update this SOP to list it under
"files to update".

## 3. Deciding the version bump (SemVer)

Pick the bump against the last tag using semantic-versioning intent:

* **PATCH** (`v1.0.1 → v1.0.2`): bug fixes, docs, tests, internal cleanups — no
  change to tool names, parameters, return payloads, or behavior.
* **MINOR** (`v1.0.1 → v1.1.0`): backward-compatible additions — new MCP tool,
  new optional parameter, new script, additive behavior.
* **MAJOR** (`v1.1.0 → v2.0.0`): backward-**incompatible** changes — renamed or
  removed tools, changed return-payload shape, changed default behavior, or a
  tightened safety gate that rejects previously-accepted input.

When unsure between two levels, choose the higher one.

## 4. Preparing a version-bump / release PR

Releases are still gated by the normal issue-first, PR-reviewed flow.

1. Open (or use) a tracking issue for the release and **claim it** with
   `status:in-progress` (see §9).
2. Create an isolated, issue-linked branch + worktree from latest `master`
   (e.g. `chore/issue-63-v1.1.0`). Never commit directly to `master`.
3. Include in the PR:
   * Any code/docs changes that belong to the release.
   * The **release notes** for the annotated tag (draft them in the PR body or a
     notes file you will pass to `scripts/release-tag --notes-file`).
   * If a `VERSION`/`CHANGELOG` file exists at that time, its update.
4. Open the PR **targeting `master`**.

The tag is **not** created in the PR. Tagging happens only after merge (§6).

## 5. Required checks before release

Run all of these green before merging the release PR and before tagging:

```bash
python3 -m py_compile mcp_server.py
python3 -m py_compile manage_labels.py
bash -n scripts/clear-provenance
./venv/bin/python -m pytest tests/ -q
git diff --check
```

Plus a secret sweep (there is no third-party scanner wired in; do a staged-diff
sweep — see [`developer-testing-guidelines.md`](developer-testing-guidelines.md)
§7):

```bash
git diff --cached | grep -nEi "authorization: (basic|bearer)|password[:=]|token=[A-Za-z0-9]" || echo "clean"
```

`scripts/release-tag` **also** runs the test suite itself before tagging (unless
`--skip-tests` is passed), so tests are enforced twice by default.

## 6. Running `scripts/release-tag`

Tag **only after** the release PR is merged to `master`. `scripts/release-tag`
enforces the tagging policy and is **safe by default** (creates nothing on a
dry-run; never pushes without `--push`).

Before it tags, it requires **all** of:

* version matches `vMAJOR.MINOR.PATCH` (SemVer);
* `fetch --prune` has run;
* you are **on `master`**;
* the worktree is **clean** (no uncommitted changes);
* local `master` **equals** `<remote>/master`;
* `HEAD` is that same commit (the commit is present on remote master);
* the tag does **not** already exist locally or on the remote;
* the test suite passes (unless `--skip-tests`, which warns).

Typical sequence:

```bash
# 1. Dry-run to confirm the plan (changes nothing)
scripts/release-tag --dry-run v1.1.0

# 2. Create the annotated tag locally, with release notes
scripts/release-tag v1.1.0 --notes-file /path/to/release-notes.md

# 3. Push the tag only when ready
scripts/release-tag v1.1.0 --notes-file /path/to/release-notes.md --push
```

Env injection points (mainly for CI/tests):
`RELEASE_TAG_REMOTE` (default `prgs`), `RELEASE_TAG_TEST_CMD`
(default `./venv/bin/python -m pytest tests/ -q`).

## 7. Who may merge / tag

* The release PR must be **merged by someone other than its author** — the
  author-cannot-merge safety gate applies to releases exactly as to any other PR.
* Merge uses the gated `gitea_merge_pr` workflow; CLI/legacy merge is disabled.
* Whoever tags must operate on clean master synced to the remote (enforced by
  `scripts/release-tag`). Tagging is an operator action performed after merge.

## 8. Self-review / self-merge restrictions

Release PRs are **not** exempt from the safety model:

* No self-review — the author may not approve their own release PR.
* No self-merge — a different eligible identity merges.
* These gates are enforced by the MCP tooling and must not be bypassed.

## 9. Handling `status:in-progress` during release work

* **Claim** the release tracking issue with `status:in-progress` before starting.
* Keep it claimed while the release PR is open and under review.
* On merge/close, the tracker-hygiene automation releases `status:in-progress`
  for issues the PR closes; if it remains after the release lands, release it
  explicitly. Do not leave a shipped release issue marked in-progress.

## 10. Branch / worktree cleanup after merge

After the release PR merges and the tag is pushed:

* Delete the remote release branch (if repo policy allows).
* Remove the local worktree and delete the local branch:

  ```bash
  git worktree remove branches/<release-worktree>
  git branch -d <release-branch>
  git worktree prune
  ```
* Confirm the root repo is clean and on `master` synced to the remote.

## 11. What NOT to do

* **No direct commits to `master`.** All changes land via reviewed PRs.
* **No force-push** (to `master` or to tags).
* **No self-merge** of a release PR.
* **No tagging before merge** — tag only commits already on remote `master`.
* **No release from a dirty worktree** — `scripts/release-tag` refuses, and so
  should you.
* **No `--skip-tests`** for a real release unless there is an explicit,
  documented reason.
* **No re-tagging / moving an existing tag** — pick the next version instead.

## 12. Post-Merge Verification & Audit Lessons (v1.1.0)

During the v1.1.0 release audit, we identified a critical reconciliation issue (captured in historical PRs/issues #68 and #82):
* **The "Closed" State Trap:** Gitea PRs marked as `closed` are not guaranteed to be `merged` (they can be closed without merging, leading to silent omissions of code/documentation changes).
* **Mandatory Post-Merge File/Commit Presence Probe:** Reviewers/mergers must perform explicit post-merge validation. Do not assume a merge succeeded.
  - Check that the merged branch head is an ancestor of the target branch (`master`):
    ```bash
    git fetch <remote> --prune
    git merge-base --is-ancestor <pr-head-sha> <remote>/master
    ```
  - Probe file presence for expected modifications/additions:
    ```bash
    git log --oneline -- <expected-file>
    # and confirm file presence:
    ls -la docs/release-version-sop.md
    ```
* **Verify in Handoff:** Final report blocks must explicitly document the verification method and probe results.
