| 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.
- Confirm the branch:
git rev-parse --abbrev-ref HEAD must print otgw-1.x.x.
- 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.
- Detect the latest prerelease tag:
git fetch --tags
PREV_TAG=$(git tag --list 'v*-*.*' --sort=-v:refname | head -1)
- Detect the latest public stable release (store as
LATEST_PUBLIC):
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)
- 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
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):
git log --pretty=format:'%h %s' "${LATEST_PUBLIC}..HEAD" -- src/OTGW-firmware/ src/libraries/ docs/
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):
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.
README.md — leave untouched. The 1.x README's "What's New in v" sections are refreshed at STABLE release (via /release), not per beta.
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)
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
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
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
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)
gh run watch --exit-status
When it exits 0, verify the release:
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)
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
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)
- 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.
- 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.
- 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).
- 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.
- 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.