# Release Process

How to release a new version of a BW plugin to `plugins.bowden.works`.

## TL;DR

```bash
cd /srv/apps/bw-plugins
tools/bump-version.sh bw-<slug> <new-version>
# Edit wp-content/plugins/bw-<slug>/CHANGELOG.md to document real changes
tools/release.sh bw-<slug> <new-version>
```

That's it. The release script handles everything else.

## What `release.sh` does

1. **Pre-flight:** verifies the argument version matches the plugin header, and `CHANGELOG.md` has a real entry for the version.
2. **Source snapshot:** tarballs the current plugin source to `.releases/bw-<slug>/<version>/source-snapshot.tar.gz` (replaces "git tag" for now).
3. **test-plugin:** PHP syntax check, header validation, version consistency across header/constant/CHANGELOG, required docs present.
4. **security-scan:** grep for dangerous patterns (eval, shell exec, SQL injection, committed secrets, TLS verify disabled, etc.).
5. **cleanup-scan:** fail if debug cruft present (var_dump, print_r, console.log, hardcoded dev URLs, empty catch blocks, TODO: REMOVE markers, etc.).
6. **Build zip:** rsync the plugin to a temp dir with exclusions (docs/, .git/, CLAUDE.md, SESSION-LOG, debug-*.php, tests/, node_modules/, dev configs).
7. **Compute SHA-256** of the zip.
8. **Publish** zip + sidecar `.meta.json` to `/srv/apps/bw-plugins-dist/wp-content/uploads/plugin-updates/`.
9. **Record** in `tools/release-history.json`.
10. **Verify** the dist host REST endpoint returns the new version.

## If any step fails

- **test-plugin fails:** fix the errors listed, re-run.
- **security-scan fails:** address the findings OR add justified entries to `bw-<slug>/.security-allowlist` (format: `<label> <file>:<line> # reason`).
- **cleanup-scan fails:** remove the debug cruft OR add justified entries to `bw-<slug>/.cleanup-allowlist`.
- **Publish fails (no write access):** fix dist host permissions; the zip is still in the snapshot dir.

Nothing is published unless ALL prior steps succeed.

## Version rules

- SemVer strict: `MAJOR.MINOR.PATCH`
- New version must be strictly greater than the current (checked via `semver_gt`)
- Version must appear in all four places (enforced):
  1. Plugin header `Version:` field
  2. `BW_<SLUG>_VERSION` constant
  3. `CHANGELOG.md` heading `## [X.Y.Z] - YYYY-MM-DD`
  4. `CLAUDE.md` header `**Version:** X.Y.Z`
- `bump-version.sh` updates all four together

## Changelog conventions

Use [Keep a Changelog](https://keepachangelog.com/):

```markdown
## [Unreleased]

## [0.2.0] - 2026-04-15

### Added
- ...

### Changed
- ...

### Fixed
- ...

### Security
- ...
```

The release script extracts the matching section from CHANGELOG.md and embeds it in the manifest, so client sites see it in the WP Admin "View Details" modal.

## What gets excluded from the release zip

Automatic exclusions (don't ship dev cruft):

```
docs/                       CLAUDE.md
tests/                      HANDOFF-NOTES.md
.git/                       SESSION-LOG.md
.github/                    debug-*.php
node_modules/               test-*.php
.DS_Store                   scratch-*.php
*.swp *.swo *~              .security-allowlist
phpcs.xml                   .cleanup-allowlist
phpunit.xml                 .editorconfig
composer.lock               .eslintrc* .prettierrc*
package-lock.json
```

## Rollback

If a release is bad:

```bash
# (Not yet implemented — planned for a future tools/rollback.sh)
# For now: manually remove the bad zip + meta from dist host, run a fresh release with the next patch version containing the fix.
```

**Important:** once clients have pulled a bad version, you can't un-publish it. Release a fixed patch version instead.

## Who can release

- Must run as user `rian` (release.sh checks).
- Must have write access to `/srv/apps/bw-plugins-dist/wp-content/uploads/plugin-updates/`.
- Future: `bw-releasers` group for collaborators.
