-
-
Notifications
You must be signed in to change notification settings - Fork 12
258 lines (237 loc) · 9.55 KB
/
Copy pathrelease-prepare.yml
File metadata and controls
258 lines (237 loc) · 9.55 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
name: Release prepare
on:
workflow_dispatch:
inputs:
bump_type:
description: 'Version bump type (ignored when version is set)'
required: false
default: patch
type: choice
options:
- patch
- minor
- major
version:
description: 'Explicit version override, e.g. 1.2.3 or 1.2.3-rc1 (ignores bump_type)'
required: false
default: ''
type: string
expected_current:
description: 'Safety check: fail if gradle.properties version does not match this value'
required: false
default: ''
type: string
dry_run:
description: 'Dry run: validate and print plan only, skip commit/push'
required: false
default: true
type: boolean
permissions:
contents: read
concurrency:
group: release-prepare-main
cancel-in-progress: false
jobs:
compute-and-validate:
name: Compute and validate
permissions:
contents: read
runs-on: ubuntu-24.04
outputs:
current_name: ${{ steps.read-version.outputs.current }}
new_name: ${{ steps.compute-version.outputs.new }}
steps:
- name: Guard — must run on main
env:
REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "$REF" != "refs/heads/main" ]]; then
echo "::error::This workflow must be dispatched from main (got $REF)"
exit 1
fi
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: false
- name: Read current version
id: read-version
env:
EXPECTED_CURRENT: ${{ inputs.expected_current }}
run: |
set -euo pipefail
count=$(grep -cE '^version=' gradle.properties || true)
[[ "$count" == "1" ]] || { echo "::error::expected exactly one 'version=' line in gradle.properties, found $count"; exit 1; }
current=$(awk -F= '$1=="version"{print $2}' gradle.properties)
echo "$current" | grep -qE '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(rc|beta)(0|[1-9][0-9]*))?$' || {
echo "::error::current version '$current' does not match semver format"
exit 1
}
if [[ -n "$EXPECTED_CURRENT" && "$current" != "$EXPECTED_CURRENT" ]]; then
echo "::error::current version '$current' does not match expected_current '$EXPECTED_CURRENT'"
exit 1
fi
echo "current=$current" >> "$GITHUB_OUTPUT"
- name: Compute next version
id: compute-version
env:
CURRENT: ${{ steps.read-version.outputs.current }}
BUMP_TYPE: ${{ inputs.bump_type }}
OVERRIDE: ${{ inputs.version }}
run: |
set -euo pipefail
new=$(python3 - <<'PYEOF'
import re, sys, os
semver = re.compile(r'^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(rc|beta)(0|[1-9][0-9]*))?$')
def parse(v):
m = semver.match(v)
if not m: sys.exit(f"invalid version: {v}")
return (int(m[1]), int(m[2]), int(m[3]), m[4], int(m[5]) if m[5] else None)
def precedence(p):
return (p[0], p[1], p[2], 1 if p[3] is None else 0, p[3] or '', p[4] or 0)
cur = parse(os.environ['CURRENT'])
override = os.environ.get('OVERRIDE') or None
if override:
nxt = parse(override)
else:
if cur[3] is not None:
sys.exit("bump_type rejected on prerelease current; use explicit version override")
x, y, z = cur[:3]
b = os.environ['BUMP_TYPE']
if b == 'patch': z += 1
elif b == 'minor': y, z = y + 1, 0
elif b == 'major': x, y, z = x + 1, 0, 0
else: sys.exit(f"invalid bump_type: {b}")
nxt = (x, y, z, None, None)
if precedence(nxt) <= precedence(cur):
sys.exit(f"refusing downgrade: {nxt} not > {cur}")
pre = f"-{nxt[3]}{nxt[4]}" if nxt[3] else ''
print(f"{nxt[0]}.{nxt[1]}.{nxt[2]}{pre}")
PYEOF
)
echo "new=$new" >> "$GITHUB_OUTPUT"
- name: Check tag collision
env:
NEW: ${{ steps.compute-version.outputs.new }}
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW}" >/dev/null 2>&1; then
echo "::error::Local tag v${NEW} already exists"
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW}" >/dev/null; then
echo "::error::Remote tag v${NEW} already exists"
exit 1
fi
- name: Write job summary
env:
CURRENT: ${{ steps.read-version.outputs.current }}
NEW: ${{ steps.compute-version.outputs.new }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|----------|-------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Current | \`$CURRENT\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| New | \`$NEW\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Tag | \`v$NEW\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Dry run | \`$DRY_RUN\` |" >> "$GITHUB_STEP_SUMMARY"
push-and-dispatch:
name: Push bump and tag
needs: compute-and-validate
if: ${{ !inputs.dry_run }}
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- name: Mint GitHub App token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1
with:
client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Resolve bot identity
id: bot
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -euo pipefail
user_id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)
echo "user_name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT"
echo "user_email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT"
- name: Checkout main with App token
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
ref: main
fetch-depth: 0
persist-credentials: true
token: ${{ steps.app-token.outputs.token }}
- name: Re-validate current version
env:
EXPECTED: ${{ needs.compute-and-validate.outputs.current_name }}
run: |
set -euo pipefail
count=$(grep -cE '^version=' gradle.properties || true)
[[ "$count" == "1" ]] || { echo "::error::expected exactly one 'version=' line, found $count"; exit 1; }
current=$(awk -F= '$1=="version"{print $2}' gradle.properties)
[[ "$current" == "$EXPECTED" ]] || {
echo "::error::current version '$current' does not match expected '$EXPECTED' (main moved during job gap?)"
exit 1
}
- name: Re-check tag collision
env:
NEW: ${{ needs.compute-and-validate.outputs.new_name }}
run: |
set -euo pipefail
if git rev-parse --verify "refs/tags/v${NEW}" >/dev/null 2>&1; then
echo "::error::Local tag v${NEW} already exists"
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/v${NEW}" >/dev/null; then
echo "::error::Remote tag v${NEW} already exists"
exit 1
fi
- name: Apply version bump
env:
NEW: ${{ needs.compute-and-validate.outputs.new_name }}
run: |
set -euo pipefail
sed -i "s/^version=.*/version=${NEW}/" gradle.properties
count=$(grep -cE '^version=' gradle.properties || true)
[[ "$count" == "1" ]] || { echo "::error::expected exactly one version= line after bump, found $count"; exit 1; }
got=$(awk -F= '$1=="version"{print $2}' gradle.properties)
[[ "$got" == "$NEW" ]] || { echo "::error::bump verification failed: expected '$NEW' got '$got'"; exit 1; }
- name: Configure git identity
env:
GIT_USER_NAME: ${{ steps.bot.outputs.user_name }}
GIT_USER_EMAIL: ${{ steps.bot.outputs.user_email }}
run: |
set -euo pipefail
git config user.name "$GIT_USER_NAME"
git config user.email "$GIT_USER_EMAIL"
- name: Commit and tag
env:
NEW: ${{ needs.compute-and-validate.outputs.new_name }}
run: |
set -euo pipefail
git add gradle.properties
git commit -m "Release: v${NEW}"
git tag -a "v${NEW}" -m "Release v${NEW}"
- name: Atomic push
env:
NEW: ${{ needs.compute-and-validate.outputs.new_name }}
run: |
set -euo pipefail
git push --atomic origin HEAD:refs/heads/main "refs/tags/v${NEW}"
- name: Write final summary
env:
NEW: ${{ needs.compute-and-validate.outputs.new_name }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
echo "Pushed commit and tag \`v${NEW}\` to main." >> "$GITHUB_STEP_SUMMARY"
echo "The \`release-tag.yml\` workflow fired automatically — track it at:" >> "$GITHUB_STEP_SUMMARY"
echo "https://github.com/${REPO}/actions/workflows/release-tag.yml" >> "$GITHUB_STEP_SUMMARY"