8.9 KiB
Release workflow
Cutting a release is fast and low-ceremony for this project. Most releases are patch bumps that go from "decision to ship" to "Telegram channel posting" in under 30 minutes of human work + ~30 minutes of CI.
When to cut a release
Cut a release whenever anything user-visible has landed since the last tag. User-visible includes:
- Bug fixes that affect runtime behavior
- New config options
- New CLI subcommands or flags
- Diagnostic improvements (better log messages, error categories)
- Apps Script script changes (Code.gs / CodeFull.gs)
- Documentation that users will read (README updates, troubleshooting docs — though these can also batch into the next release)
Don't cut for:
- Internal refactors with no behavior change
- CI/workflow file edits
- Markdown formatting fixes
- Test-only changes
When in doubt, cut. Patch releases are cheap and Iranian users actively check the Telegram channel for updates.
The release workflow
Step 1: Decide the version
Read the latest tag:
git describe --tags --abbrev=0
Then bump:
- Patch (Z+1) — for ~95% of releases. v1.8.2 → v1.8.3
- Minor (Y+1) — for a coherent feature batch shipped together. v1.7.x → v1.8.0 represented "DPI evasion + active-probing defense + full-mode usage counters" together
- Major (X+1) — never done in this project's history. Reserved for true protocol-incompatible changes with the Apps Script side. Don't bump major without explicit go-ahead.
Step 2: Bump Cargo.toml
Edit Cargo.toml line 3 (version = "X.Y.Z"). Keep package name mhrv-rs unchanged. The tunnel-node subcrate has its own version that's independent — don't bump it unless you're shipping a tunnel-node change.
Step 3: Build to refresh Cargo.lock
cargo build --release 2>&1 | tail -3
Cargo.lock will pick up the new version string. Verify with:
git diff Cargo.lock | head -20
Should show only the name = "mhrv-rs" block's version = "X.Y.Z" change.
Step 4: Write the changelog
Create docs/changelog/vX.Y.Z.md using the format in assets/changelog-template.md. Persian first, then ---, then English. See workflow-conventions.md for format details.
When the release is shipping multiple PRs from contributors, credit each by name + handle in both halves of the changelog.
Step 5: Run tests + final build
cargo test --lib 2>&1 | tail -5
cargo build --release 2>&1 | tail -3
cargo build --bin mhrv-rs-ui --release --features ui 2>&1 | tail -3
All three must succeed. Test count varies by version. All passing is the gate.
If any contributor PRs were merged in this release, also verify by re-running tests after the merge — sometimes integration with main reveals issues that didn't show in the PR's CI.
Step 6: Commit + tag + push
git add Cargo.toml Cargo.lock docs/changelog/vX.Y.Z.md
git status # sanity check
git commit -m "$(cat <<'EOF'
chore: vX.Y.Z — <short summary fitting under 75 chars>
<longer body explaining the why and the changes — see workflow-conventions.md
for format>
EOF
)"
git push origin main
git tag vX.Y.Z
git push origin vX.Y.Z
The git push origin vX.Y.Z is the trigger — release CI auto-fires on tag push.
If git push origin main fails with non-fast-forward, someone (often the auto-binary-refresh CI from a prior release) pushed in the meantime:
git pull --rebase origin main
git push origin main
git tag vX.Y.Z # if you didn't tag yet
git push origin vX.Y.Z
If you already tagged before the push race, the tag still works — it's pinned to your commit, and the rebase shouldn't change your commit's SHA unless there were conflicts.
Step 7: Watch CI
gh run list --limit 3
Two workflows fire on tag push:
release-drafter— quick (~15s), updates the GitHub release draft. Always succeeds.release— slow (~25-35 minutes), builds binaries for all platforms, attaches to release.
Once release succeeds, a third workflow auto-fires:
3. Telegram publish release files — posts each binary individually to the Telegram channel mhrv_rs with Persian captions, SHA-256 hashes, and a cross-link from the main channel. Takes ~1-2 minutes.
If release fails, common causes:
- Cross-compile failure — particularly on i686 / mipsel. i686 was dropped from the matrix in v1.7.11 because of MSRV churn (see #411 thread). If a new architecture starts failing, it's usually a transitive dep bumping MSRV past what the toolchain pinned for that target supports. Triage: check which architecture's job failed, look at the cargo error, decide whether to pin a dep with
cargo update --preciseor drop the architecture. actions/download-artifact@v4flakiness — replaced withgh run download+ 3-attempt retry in v1.7.11. Should be stable now; if it flakes again, increase retry count.
After CI succeeds, optionally check the binary refresh:
git pull origin main
git log --oneline -3
You should see an auto-generated commit chore(releases): refresh prebuilt binaries for vX.Y.Z from the release CI bot.
Step 8: Verify Telegram channel
The Telegram publish workflow posts to channel mhrv_rs (public link https://t.me/mhrv_rs). The channel should show:
- An announcement post:
📦 mhrv-rs vX.Y.Z منتشر شد...referencing the changelog file - ~16 individual file posts (Android APKs split by ABI, Windows ZIP, macOS arm64/amd64 dmg+tar, Linux x86_64/arm64 incl. musl, Raspbian, OpenWRT)
- Each file caption includes Persian description (e.g., "نسخه ویندوز x86") + SHA-256 hash
- A "main channel" post (different channel) cross-linking to the files channel post
Files larger than 50 MB get chunked into .part_aa, .part_ab, etc. via the split pattern in .github/scripts/telegram_publish_files.py.
If something didn't post, check the workflow run logs:
gh run view <run-id> --log
Common cause: the auto-fire dispatch on workflow_run requires the parent workflow to succeed; if release.yml had a flaky download retry, the dispatch might still succeed but partial.
Manual re-publish (rare)
If you need to re-trigger Telegram publishing for an already-released version (e.g., the workflow failed and you fixed it), use workflow_dispatch:
gh workflow run "Telegram publish release files" -f version=vX.Y.Z
The script downloads artifacts via gh release download (not the workflow's artifacts) so it works retroactively.
Re-cutting a release (very rare)
If a release was tagged and pushed but turns out to be broken (e.g., bug in a merged PR you wanted to revert):
- Don't delete the tag if the release is already public. Iranian users may have already pulled the binaries; a deleted tag confuses them and they think the project is gone.
- Cut a fix immediately as the next patch (vX.Y.Z+1).
- Optionally edit the GitHub release notes for the broken version to say "known issue, upgrade to vX.Y.Z+1".
If you tagged but didn't push yet, just delete the tag locally and re-tag after fixing:
git tag -d vX.Y.Z # local only; safe
# fix the issue, commit
git tag vX.Y.Z
git push origin vX.Y.Z
Pre-release rollback
If cargo test --lib fails after merging PRs but before tagging:
- Don't tag.
- Either revert the merge commit (
git revert <merge-commit-sha>) or fix forward (commit a new fix on main). - Re-run tests until green.
- Tag.
The release CI doesn't run tests before building, so untagged-but-broken main is fine — you have time to fix before tagging.
Coordinating with multiple PRs in flight
If two PRs are both ready to merge, the order matters:
- Merge them one at a time (not both into a single tag) only if they're independent
- If they touch the same files, merge them sequentially with
gh pr checkout N1 && cargo test && merge, thengh pr checkout N2(which now bases on the new main; CI on the PR may show the old base, but the local checkout sees latest main)&& cargo test && merge - If a merge introduces conflicts, GitHub's UI flags the PR as conflicting; resolve via
gh pr checkout N+ manual rebase + push to the PR branch
After all PRs are merged, then bump version, write changelog (covering all merged PRs), tag, push.
Versioning the tunnel-node subcrate
tunnel-node/Cargo.toml has its own version field separate from the main crate. Bump it when:
- Changing the tunnel-node HTTP API (
/tunnel,/batchendpoints) - Changing the auth flow (
TUNNEL_AUTH_KEYsemantics) - Changing the env var contract
- Bumping the Docker image label
For pure internal refactors of tunnel-node that don't change the surface, leave it alone — the Docker image at ghcr.io/therealaleph/mhrv-tunnel-node:latest continues to be the latest tag and users don't need to know an internal version bumped.
When tunnel-node version bumps, the Docker image gets re-tagged in the registry by the CI. Users running docker pull ghcr.io/therealaleph/mhrv-tunnel-node:latest get the new version automatically; users pinned to a specific version stay pinned.