-
Notifications
You must be signed in to change notification settings - Fork 633
343 lines (317 loc) · 14.5 KB
/
Copy pathcreate-release.yaml
File metadata and controls
343 lines (317 loc) · 14.5 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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
name: Create Release
on:
workflow_dispatch:
inputs:
version_bump:
description: "Version bump type (hotfix branches force 'patch')"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
confirm_major:
description: "Confirm major version bump (required when 'major' is selected)"
required: false
type: boolean
default: false
pre_release:
description: "Create as pre-release"
required: false
type: boolean
default: false
dry_run:
description: "Dry run - only compute and display the next version without creating a release"
required: false
type: boolean
default: false
concurrency:
group: release-creation
cancel-in-progress: false
run-name: "Create Release (${{ github.event.inputs.version_bump }}${{ github.event.inputs.dry_run == 'true' && ' - dry run' || '' }})"
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
defaults:
run:
shell: bash -euo pipefail {0}
steps:
# A GitHub App token is required (not the default GITHUB_TOKEN) because releases
# created with GITHUB_TOKEN do not trigger downstream `release: created` workflows
# due to GitHub's anti-recursion policy. Same pattern used in unstract-cloud.
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.PUSH_TO_MAIN_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_TO_MAIN_APP_PRIVATE_KEY }}
owner: Zipstack
repositories: |
unstract
- name: Validate branch and determine release mode
id: branch-mode
env:
BRANCH: ${{ github.ref_name }}
run: |
if [[ "$BRANCH" == "main" ]]; then
MODE=main
HOTFIX_LINE=""
elif [[ "$BRANCH" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)-hotfix$ ]]; then
MODE=hotfix
HOTFIX_LINE="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
echo "Hotfix branch detected. Line: v${HOTFIX_LINE}.x"
else
echo "::error::Releases must be cut from 'main' or a 'vX.Y.Z-hotfix' branch. Got: '$BRANCH'"
exit 1
fi
echo "mode=$MODE" >> "$GITHUB_OUTPUT"
echo "hotfix_line=$HOTFIX_LINE" >> "$GITHUB_OUTPUT"
- name: Validate inputs for hotfix mode
if: steps.branch-mode.outputs.mode == 'hotfix'
env:
BUMP: ${{ github.event.inputs.version_bump }}
run: |
if [[ "$BUMP" != "patch" ]]; then
echo "::error::Hotfix branches only support 'patch' bumps. Got: '$BUMP'"
exit 1
fi
- name: Validate major bump confirmation
if: github.event.inputs.version_bump == 'major' && github.event.inputs.confirm_major != 'true'
run: |
echo "::error::Major version bump requires the 'Confirm major version bump' checkbox to be checked."
exit 1
- name: Fetch base release version
id: get-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODE: ${{ steps.branch-mode.outputs.mode }}
HOTFIX_LINE: ${{ steps.branch-mode.outputs.hotfix_line }}
run: |
if [[ "$MODE" == "main" ]]; then
# Use the "latest" API (releases/latest) which respects the explicit
# "Set as latest release" flag. Hotfix releases are created with
# --latest=false, so this never returns a hotfix tag.
LATEST_TAG=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name // empty')
else
# For hotfixes, find the latest tag on this specific v<major>.<minor>.x line
LATEST_TAG=$(gh release list --repo "${{ github.repository }}" \
--limit 100 --exclude-drafts --exclude-pre-releases \
--json tagName \
--jq "[.[] | select(.tagName | startswith(\"v${HOTFIX_LINE}.\"))] | .[0].tagName // empty")
fi
if [[ -z "$LATEST_TAG" ]]; then
echo "::error::Could not find a base release to bump from (mode=$MODE)."
exit 1
fi
if [[ ! "$LATEST_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Base release tag '$LATEST_TAG' does not match expected format 'vX.Y.Z'"
exit 1
fi
echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
echo "Base release: $LATEST_TAG"
- name: Ensure there are new commits to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
BRANCH: ${{ github.ref_name }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
# Reject a no-op release: if the branch has no commits beyond the last release
# tag (e.g. main is already at v0.177.0), there is nothing to release. This
# prevents empty releases such as v0.177.1 pointing at the same commit as
# v0.177.0. The workflow has no local checkout, so use the compare API's
# `ahead_by` (commits on BRANCH not in LATEST_TAG). `// empty` stops a null/
# absent value from reaching the comparison as the literal "null"; we then
# require a non-negative integer and fail loudly on anything unexpected, so a
# transient API error is never mistaken for "no new commits".
AHEAD=$(gh api "repos/${{ github.repository }}/compare/${LATEST_TAG}...${BRANCH}" --jq '.ahead_by // empty') \
|| { echo "::error::Could not query commits since $LATEST_TAG (compare API failed)."; exit 1; }
if ! [[ "$AHEAD" =~ ^[0-9]+$ ]]; then
echo "::error::Unexpected ahead_by='${AHEAD:-<empty>}' from compare ${LATEST_TAG}...${BRANCH}; refusing to guess."
exit 1
fi
if [[ "$AHEAD" == "0" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
echo "::warning::No new commits on '$BRANCH' since $LATEST_TAG — a real run would be rejected (nothing to release)."
else
echo "::error::No new commits on '$BRANCH' since $LATEST_TAG — nothing to release. Aborting to avoid an empty release."
exit 1
fi
else
echo "✅ $AHEAD new commit(s) on '$BRANCH' since $LATEST_TAG"
fi
- name: Compute next version
id: compute-version
env:
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
BUMP_TYPE: ${{ github.event.inputs.version_bump }}
run: |
VERSION="${LATEST_TAG#v}"
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
case "$BUMP_TYPE" in
patch)
PATCH=$((PATCH + 1))
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
*)
echo "::error::Unknown bump type: '$BUMP_TYPE'"
exit 1
;;
esac
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
# Strict monotonic increase: equal is NOT allowed, sort -V -C alone
# accepts equal values (it checks non-decreasing order).
CURRENT_NUM="${LATEST_TAG#v}"
NEW_NUM="${NEW_TAG#v}"
if [[ "$CURRENT_NUM" == "$NEW_NUM" ]]; then
echo "::error::Computed version $NEW_TAG equals current version $LATEST_TAG"
exit 1
fi
if ! printf '%s\n%s\n' "$CURRENT_NUM" "$NEW_NUM" | sort -V -C; then
echo "::error::Computed version $NEW_TAG is not greater than current version $LATEST_TAG"
exit 1
fi
echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT"
echo "Current: $LATEST_TAG -> New: $NEW_TAG"
- name: Check for tag collision
id: collision-check
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
run: |
# Guard against collisions with existing pre-releases or drafts
# (which are hidden by --exclude-pre-releases in the earlier lookup).
if gh release view "$NEW_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "::error::A release with tag '$NEW_TAG' already exists (possibly as a pre-release or draft). Delete it first or pick a different bump type."
exit 1
fi
- name: Dry run summary
if: github.event.inputs.dry_run == 'true'
env:
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
BUMP: ${{ github.event.inputs.version_bump }}
PRERELEASE: ${{ github.event.inputs.pre_release }}
MODE: ${{ steps.branch-mode.outputs.mode }}
run: |
{
echo "## Dry Run Summary"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| Mode | $MODE |"
echo "| Current version | $LATEST_TAG |"
echo "| Bump type | $BUMP |"
echo "| Next version | $NEW_TAG |"
echo "| Pre-release | $PRERELEASE |"
echo ""
echo "No release was created (dry run mode)."
} >> "$GITHUB_STEP_SUMMARY"
- name: Create GitHub release
id: create-release
if: github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
MODE: ${{ steps.branch-mode.outputs.mode }}
PRERELEASE: ${{ github.event.inputs.pre_release }}
TARGET_BRANCH: ${{ github.ref_name }}
run: |
RELEASE_ARGS=(
"$NEW_TAG"
--title "$NEW_TAG"
--target "$TARGET_BRANCH"
--generate-notes
--notes-start-tag "$LATEST_TAG"
)
# Decide whether this release should become GitHub "latest".
#
# Rule: mark latest iff this version supersedes the current "latest" release
# (i.e. it is the newest stable release by semver). This replaces the old
# main-only heuristic, which left hotfixes on the CURRENT production line
# (e.g. latest v0.176.3, hotfix -> v0.176.4) incorrectly marked latest=false.
# Hotfixes on OLDER lines still stay latest=false because they do not
# supersede the current latest.
if [[ "$PRERELEASE" == "true" ]]; then
# A pre-release is never GitHub "latest" — skip the supersession check so we
# never emit --latest=true alongside --prerelease.
echo "ℹ️ Pre-release $NEW_TAG - not marking latest"
RELEASE_ARGS+=(--latest=false)
else
# Query the current "latest" release, distinguishing a genuine 404 (no latest
# release yet) from a transient API failure (rate limit / 5xx). On a transient
# failure we must NOT silently fall through to latest=true, or a back-version
# hotfix could wrongly steal "latest" — fail the job loudly instead. (Mirrors
# the existing guarded pattern in the "Fetch base release version" step above.)
if CURRENT_LATEST=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name // empty' 2>/tmp/gh_latest.err); then
:
elif grep -qiE 'HTTP 404|Not Found' /tmp/gh_latest.err; then
CURRENT_LATEST="" # genuinely no latest release yet
else
echo "::error::Could not determine the current latest release (transient gh API failure); refusing to guess the 'latest' flag."
cat /tmp/gh_latest.err >&2
exit 1
fi
if [[ -z "$CURRENT_LATEST" ]]; then
echo "✅ No existing 'latest' release - marking $NEW_TAG as latest"
RELEASE_ARGS+=(--latest=true)
elif [[ "$NEW_TAG" == "$CURRENT_LATEST" ]]; then
# Defensive/unreachable: the "Check for tag collision" step rejects a NEW_TAG
# that already exists, and CURRENT_LATEST is an existing release, so the two
# cannot be equal here. Kept only as a guard against a tie if this block is
# ever reused without that upstream check.
echo "ℹ️ $NEW_TAG equals current latest $CURRENT_LATEST - leaving latest unchanged"
RELEASE_ARGS+=(--latest=false)
else
NEWEST=$(printf '%s\n%s\n' "$NEW_TAG" "$CURRENT_LATEST" | sort -V | tail -n1)
if [[ "$NEWEST" == "$NEW_TAG" ]]; then
echo "✅ $NEW_TAG supersedes current latest $CURRENT_LATEST (mode: $MODE) - marking as latest"
RELEASE_ARGS+=(--latest=true)
else
echo "ℹ️ $NEW_TAG does not supersede current latest $CURRENT_LATEST (mode: $MODE) - NOT marking as latest"
RELEASE_ARGS+=(--latest=false)
fi
fi
fi
if [[ "$PRERELEASE" == "true" ]]; then
RELEASE_ARGS+=(--prerelease)
fi
gh release create --repo "${{ github.repository }}" "${RELEASE_ARGS[@]}"
echo "Release $NEW_TAG created successfully."
- name: Summary
if: success() && github.event.inputs.dry_run != 'true'
env:
LATEST_TAG: ${{ steps.get-latest.outputs.latest_tag }}
NEW_TAG: ${{ steps.compute-version.outputs.new_tag }}
BUMP: ${{ github.event.inputs.version_bump }}
PRERELEASE: ${{ github.event.inputs.pre_release }}
MODE: ${{ steps.branch-mode.outputs.mode }}
run: |
{
echo "## Release Created"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| Mode | $MODE |"
echo "| Previous version | $LATEST_TAG |"
echo "| New version | $NEW_TAG |"
echo "| Bump type | $BUMP |"
echo "| Pre-release | $PRERELEASE |"
echo "| Triggered by | ${{ github.actor }} |"
echo ""
echo "The [production build](${{ github.server_url }}/${{ github.repository }}/actions/workflows/production-build.yaml) workflow will be triggered automatically."
} >> "$GITHUB_STEP_SUMMARY"