---
name: beta-prerelease
description: Publish an OTGW-firmware beta prerelease — bump _VERSION_PRERELEASE, push to otgw-1.x.x, tag, and let CI build + publish the GitHub prerelease
disable-model-invocation: true
---

# /beta-prerelease - OTGW-firmware Beta Prerelease Skill

Publish a single beta build to field testers in Discord `#beta-testing`. Lightweight, repeatable many times within one minor cycle. Does NOT merge to main, does NOT touch the stable `_SEMVER_CORE`.

## Usage

```
/beta-prerelease
```

No arguments. Auto-detects the current `_VERSION_PRERELEASE` and increments it via `bin/bump-prerelease.sh`.

## Token-efficiency rules (apply throughout all phases)

- **P1 Build output**: Pipe through `tee .tmp/build_beta.log | tail -5`. Only read `.tmp/build_beta.log` if exit code != 0.
- **P2 Phase 3 reads**: Use targeted `grep`/`sed` to extract only the relevant section from each file. Never read README, CHANGELOG, or RELEASE_NOTES in full.
- **P3 Phase 3 writes**: Edit each file and note "Updated." immediately. Do NOT keep all edited content in context — keep only the filename.
- **P4 Known Traps**: Summarized as 5 bullets below. Read `.github/workflows/beta-prerelease.yml` only if a trap is actually hit.
- **P5 Phase 8 CI poll**: Use `gh run watch` instead of opening a browser.

## When to use

Run when a firmware change under `src/OTGW-firmware/**` or `src/libraries/**` is committed and ready for field testing. Do NOT use for docs-only commits or full releases to `main` (use `/release <version>` for those).

## How this differs from /release

| Aspect | `/release` | `/beta-prerelease` |
|---|---|---|
| Target branch | `dev` then merge to `main` | `otgw-1.x.x` only |
| Bumps `_SEMVER_CORE` | yes | no |
| GitHub release | stable, not prerelease | prerelease: true |
| Discord channels | `#nederlandse-ondersteuning`, `#english-support` | `#beta-testing` |
| Mandatory checkpoints | 2 | 1 (Discord announcement) |

## Writing style rules

- **Never use em dashes** — use colons, periods, commas, or parentheses instead.
- **All release text MUST be in English** (international audience).
- **No emojis** in release notes or Discord posts.

## Process

### Phase 0: Prepare — clean state on otgw-1.x.x

The 1.x maintenance/LTS line lives in its own worktree. Run this skill FROM the
`wt-otgw-1.x.x` worktree. Do NOT `git checkout otgw-1.x.x` inside the dev tree:
the branch is already checked out in the worktree and the checkout will fail.

1. Confirm the branch: `git rev-parse --abbrev-ref HEAD` must print `otgw-1.x.x`.
2. Verify clean state: `git status` must be clean (or only the firmware change about to be bumped). Then `git pull origin otgw-1.x.x`.
3. Detect the latest prerelease tag:
   ```bash
   git fetch --tags
   PREV_TAG=$(git tag --list 'v*-*.*' --sort=-v:refname | head -1)
   ```
4. Detect the latest public stable release (store as `LATEST_PUBLIC`):
   ```bash
   LATEST_PUBLIC=$(gh release view --json tagName --jq '.tagName' 2>/dev/null \
     || git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -v -- '-' | head -1)
   ```
5. Read current prerelease: `grep _VERSION_PRERELEASE src/OTGW-firmware/version.h`

### Phase 1: ADR validation

Skip by default. Only pause if the staged firmware change introduces a new architectural pattern, dependency, or NFR shift. Most beta cycles do not need this gate.

### Phase 2: Bump prerelease

```bash
bin/bump-prerelease.sh
```

Prints the transition (e.g. `beta.3 -> beta.4`). Store the new value as `NEW_PRERELEASE`. The helper does NOT git-add — you stage in Phase 6.

Assemble the tag: `TAG="v${SEMVER_CORE}-${NEW_PRERELEASE}"`

### Phase 3: Refresh README + CHANGELOG + RELEASE_NOTES (mandatory, P2, P3)

The GitHub Action reads these files at the tagged commit. Stale narrative at the tag = stale release page (Trap 1). Refresh immediately after the bump so `NEW_PRERELEASE` is known.

**Staleness check (P2 — targeted extractions, not full reads):**

```bash
# 1. Commits since last public release (the change set to account for)
git log --pretty=format:'%h %s' "${LATEST_PUBLIC}..HEAD" -- src/OTGW-firmware/ src/libraries/ docs/

# 2. Existing narrative — CHANGELOG [Unreleased] is the rolling beta log on the
#    1.x line. (The README "What's New in v<stable>" sections are refreshed at
#    STABLE release, not per beta, and there is no RELEASE_NOTES_*-beta file.)
grep -A 35 "## \[Unreleased\]" CHANGELOG.md | head -40
```

**Decision:**
- Every commit subject since `LATEST_PUBLIC` appears under CHANGELOG `## [Unreleased]` → pass silently, continue to Phase 4.
- Any commit missing → refresh the CHANGELOG now. Stop and ask only if the gap is ambiguous; otherwise edit in-session.

**Authoring rules (P3 — write immediately, keep only filename in context):**

1. `CHANGELOG.md` — append under `## [Unreleased]` using Keep-a-Changelog headings (`### Added/Changed/Fixed/Removed/Documentation`). One bullet per change, with ADR/TASK/PR/GH-issue reference. → note "Updated." This is the only mandatory narrative for a 1.x beta.
2. `README.md` — leave untouched. The 1.x README's "What's New in v<stable>" sections are refreshed at STABLE release (via `/release`), not per beta.
3. `RELEASE_NOTES_<next-stable>.md` (e.g. `RELEASE_NOTES_1.7.1.md`) — optional during a beta cycle (authored in full at stable release). Update it now only if you keep a running draft. → note "Updated."

Skip the CHANGELOG edit only when this is a re-cut at the same change surface (previous tag hit Trap 2). Note the reason in the commit message.

### Phase 4: Build verification (P1)

```bash
mkdir -p .tmp
python build.py --firmware 2>&1 | tee .tmp/build_beta.log | tail -5
echo "Exit: $?"
```

Must exit 0. On failure: read `.tmp/build_beta.log` for diagnosis, fix, retry. Do NOT push a tag on a broken build.

### Phase 5: Evaluator

```bash
python evaluate.py --quick
```

Must show no new failures. Pre-existing baseline failures unrelated to this change: document in the commit message.

### Phase 6: Commit and push to otgw-1.x.x

```bash
# The 1.x bin/bump-prerelease.sh updates version banners across ~24 files and
# does NOT auto-stage. Stage the real change set (on Windows, --ignore-cr-at-eol
# filters out EOL-only churn). On a clean Phase-0 tree this is exactly the bump
# banners + version.h + data/version.hash + your firmware change + CHANGELOG.md:
git add $(git diff --ignore-cr-at-eol --name-only)

git commit -m "chore(release): ${NEW_PRERELEASE}

<one-line summary of what is in this beta>"

git push origin otgw-1.x.x
```

If the pre-commit hook blocks: re-stage `version.h` + `data/version.hash` and retry. Do NOT bypass with `OTGW_BUMP_HOOK_DISABLE=1`.

Note: the `.githooks/commit-msg` task-hook requires a TASK-NNN whose `backlog/tasks/task-NNN-*.md` is tracked in THIS worktree. The backlog lives in the dev tree, so a 1.x firmware commit usually cannot satisfy it. Use a `chore(release): ...` subject (exempt from the task-hook) for the bump commit, or `OTGW_TASK_HOOK_DISABLE=1` if you reference a cross-tree TASK-NNN.

### Phase 7: Create and push the prerelease tag

```bash
SEMVER_CORE=$(grep '_SEMVER_CORE ' src/OTGW-firmware/version.h | awk -F'"' '{print $2}')
TAG="v${SEMVER_CORE}-${NEW_PRERELEASE}"

git tag -a "${TAG}" -m "Beta prerelease ${NEW_PRERELEASE}"
git push origin "${TAG}"
```

The push fires `.github/workflows/beta-prerelease.yml`. CI builds firmware + filesystem, creates a GitHub prerelease, uploads `.ino.bin`, `.littlefs.bin`, `SHA256SUMS`, flash scripts, and the flash-bundle zip.

### Phase 8: Wait for the GitHub Action (P5)

```bash
gh run watch --exit-status
```

When it exits 0, verify the release:
```bash
gh release view "${TAG}" --json tagName,isPrerelease,assets \
  --jq '{tag: .tagName, prerelease: .isPrerelease, assets: [.assets[].name]}'
```

Expected assets: `*.ino.bin`, `*.littlefs.bin`, `SHA256SUMS`, `flash_otgw.sh`, `flash_otgw.bat`, `OTGW-firmware-*-flash-bundle.zip`.

On failure: inspect logs with `gh run view --log-failed`, fix the issue, and re-run via `workflow_dispatch` or push a new tag.

### Phase 9: Discord announcement (CHECKPOINT)

Prepare announcement for `#beta-testing` (channel ID `914498730001072149`). Diff link points at `LATEST_PUBLIC` (testers want to see what changed since the last stable, not since a previous beta).

```
Beta ${NEW_PRERELEASE} is up.

Version: ${SEMVER_CORE}-${NEW_PRERELEASE}
What is new: <one or two sentences>
Download: https://github.com/rvdbreemen/OTGW-firmware/releases/tag/${TAG}
Diff vs ${LATEST_PUBLIC}: https://github.com/rvdbreemen/OTGW-firmware/compare/${LATEST_PUBLIC}...${TAG}
Changelog: https://github.com/rvdbreemen/OTGW-firmware/blob/${TAG}/CHANGELOG.md

Please flash and report findings here (good and bad).
```

**CHECKPOINT: Show the announcement to the user before sending.**

## Dry-run (testing without publishing)

```bash
git checkout -b test/beta-prerelease-dryrun
git tag -a v0.0.0-beta.dryrun -m "dryrun" && git push origin v0.0.0-beta.dryrun
# watch the Action, then clean up:
gh release delete v0.0.0-beta.dryrun --yes
git push --delete origin v0.0.0-beta.dryrun && git tag -d v0.0.0-beta.dryrun
git checkout otgw-1.x.x && git branch -d test/beta-prerelease-dryrun
```

## Known traps (P4 — full detail in `beta-prerelease.yml`)

1. **Trap 1: Immutable-releases locks publish** — upload after `gh release create` returns HTTP 422. Workaround already in CI: draft-first, attach all assets, then flip `--draft=false`. If you hit this manually, re-tag under the next beta number.
2. **Trap 2: Deleted immutable release reserves the tag forever** — even after deletion, the tag cannot be reused. Bump the prerelease number and note the skipped tag in the commit message.
3. **Trap 3: `GITHUB_TOKEN` events do not chain** — `release-assets.yml` (triggered by `release: published`) does not fire when the release is created by `GITHUB_TOKEN`. `beta-prerelease.yml` is therefore self-contained (generates SHA256SUMS + zip + flash scripts itself).
4. **Trap 4: Diff link to previous beta hides what testers care about** — always link vs `LATEST_PUBLIC` (the non-prerelease "Latest" release), not the previous beta tag.
5. **Trap 5: Stale narrative at tagged commit is permanent** — Phase 3 refresh runs before the tag is pushed for this reason. Skipping Phase 3 = stale release page that cannot be edited after publish.

## Important rules

- **Never use em dashes** in any generated text.
- **Always push to remote after every commit**.
- **Never force-push to otgw-1.x.x**.
- **Build and evaluator gates are mandatory** — do not push a tag if either is red.
- **One checkpoint**: the Discord announcement in Phase 9.
- **Do NOT bypass the bump-check hook** with `OTGW_BUMP_HOOK_DISABLE=1` — if the hook blocks, you forgot to stage `version.h` / `data/version.hash`.
