-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathauto-release-on-version-bump.yml
More file actions
244 lines (219 loc) · 10.5 KB
/
Copy pathauto-release-on-version-bump.yml
File metadata and controls
244 lines (219 loc) · 10.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
name: auto-release-on-version-bump
# Watches main for version bumps in BOTH SDK manifests. If both moved to a new
# shared version and no Release for that version exists yet, creates a GitHub
# Release at the bump commit. That Release event triggers publish.yml, which
# runs preflight + the release env approval gate before actually publishing.
#
# Rerunning this workflow on the same commit is a no-op: if the Release or tag
# already exists, we detect it and exit cleanly. Never fail loud just because
# a previous run partially succeeded.
on:
push:
branches: [main]
paths:
- 'browser-use-node/package.json'
- 'browser-use-python/pyproject.toml'
permissions:
contents: read
concurrency:
# Serialize bump-detection per branch so two simultaneous merges can't race
# to create duplicate Releases. cancel-in-progress is false so a long-running
# detector isn't killed by a follow-up push.
group: auto-release-${{ github.ref }}
cancel-in-progress: false
jobs:
detect-and-release:
name: Detect version bump and create Release
runs-on: ubuntu-latest
permissions:
contents: write # required to create the Release/tag
outputs:
should_release: ${{ steps.detect.outputs.should_release }}
version: ${{ steps.detect.outputs.version }}
tag: v${{ steps.detect.outputs.version }}
steps:
- name: Checkout (full history for range-aware diff)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve push range
id: range
env:
BEFORE: ${{ github.event.before }}
run: |
set -euo pipefail
# github.event.before is the SHA at HEAD before this push.
# For a first push to a new branch it's "0000000000000000000000000000000000000000"
# (the all-zero ref); fall back to HEAD~1 in that edge case so the
# detector still works. On main with branch protection in place,
# before should always be a real commit.
ZERO="0000000000000000000000000000000000000000"
if [ -n "${BEFORE:-}" ] && [ "$BEFORE" != "$ZERO" ]; then
BASE="$BEFORE"
SOURCE="github.event.before"
else
BASE="HEAD~1"
SOURCE="HEAD~1 (fallback; github.event.before was empty or zero-sha)"
fi
# Resolve to a real SHA for downstream use; HEAD~1 isn't a SHA literal.
BASE_SHA="$(git rev-parse "$BASE")"
echo "Using base: ${BASE_SHA} (from ${SOURCE})"
echo "::notice::Comparing ${BASE_SHA}..HEAD (source: ${SOURCE})"
echo "base_sha=${BASE_SHA}" >> "$GITHUB_OUTPUT"
- name: Detect version bump
id: detect
env:
BASE_SHA: ${{ steps.range.outputs.base_sha }}
run: |
set -euo pipefail
# Fast path: if neither manifest changed across the push range, skip.
# This catches every non-release push (most pushes) before any parsing.
CHANGED=$(git diff --name-only "${BASE_SHA}..HEAD" -- \
'browser-use-node/package.json' \
'browser-use-python/pyproject.toml' \
|| true)
if [ -z "${CHANGED}" ]; then
echo "::notice::Neither manifest changed across ${BASE_SHA}..HEAD. Skipping."
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Manifest paths touched in this push:"
echo "${CHANGED}"
# Read previous (at BASE_SHA) and current versions from both manifests.
# If a manifest didn't exist at BASE_SHA (brand-new file), the
# default "missing" lets the partial-bump branch catch it.
PREV_TS=$(git show "${BASE_SHA}:browser-use-node/package.json" 2>/dev/null | jq -r .version || echo "missing")
NEW_TS=$(jq -r .version browser-use-node/package.json)
PREV_PY=$(git show "${BASE_SHA}:browser-use-python/pyproject.toml" 2>/dev/null | grep -E '^version = ' | head -1 | sed -E 's/version = "(.*)"/\1/' || echo "missing")
NEW_PY=$(grep -E '^version = ' browser-use-python/pyproject.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
echo "TS: $PREV_TS -> $NEW_TS"
echo "PY: $PREV_PY -> $NEW_PY"
# Both manifests must report the same new version.
if [ "$NEW_TS" != "$NEW_PY" ]; then
echo "::error::TS package.json ($NEW_TS) and Python pyproject.toml ($NEW_PY) disagree. Run 'task version:bump' to sync both."
exit 1
fi
# If the version didn't change across the range, this wasn't a bump.
if [ "$NEW_TS" = "$PREV_TS" ] && [ "$NEW_PY" = "$PREV_PY" ]; then
echo "::notice::No version bump detected ($NEW_TS unchanged across ${BASE_SHA}..HEAD). Skipping."
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Partial bump: one manifest moved, the other didn't. Bail loud.
if [ "$NEW_TS" != "$PREV_TS" ] && [ "$NEW_PY" = "$PREV_PY" ]; then
echo "::error::TS bumped ($PREV_TS -> $NEW_TS) but Python didn't ($PREV_PY). Run 'task version:bump' to keep them in sync."
exit 1
fi
if [ "$NEW_TS" = "$PREV_TS" ] && [ "$NEW_PY" != "$PREV_PY" ]; then
echo "::error::Python bumped ($PREV_PY -> $NEW_PY) but TS didn't ($PREV_TS). Run 'task version:bump' to keep them in sync."
exit 1
fi
echo "::notice::Version bump detected: $PREV_TS -> $NEW_TS"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=$NEW_TS" >> "$GITHUB_OUTPUT"
- name: Check if Release already exists for this version
id: existence
if: steps.detect.outputs.should_release == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -euo pipefail
# Existence check is best-effort. We re-check at create time, so a
# race here is non-fatal.
if gh release view "v${VERSION}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "::notice::Release v${VERSION} already exists. Will skip creation."
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Check if tag already exists (separate from Release)
id: tag_existence
if: steps.detect.outputs.should_release == 'true' && steps.existence.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.detect.outputs.version }}
run: |
set -euo pipefail
# A tag may exist without a Release if someone pushed it manually
# (e.g., `git tag v3.8.2 && git push --tags`). gh release create
# against an existing tag attaches the Release to that tag rather
# than failing, so this is informational.
if gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "::notice::Tag v${VERSION} already exists (no Release attached). Will create Release against existing tag."
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
else
echo "tag_exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub Release (idempotent)
if: steps.detect.outputs.should_release == 'true' && steps.existence.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.detect.outputs.version }}
TAG_EXISTS: ${{ steps.tag_existence.outputs.tag_exists }}
run: |
set -euo pipefail
# If the tag already exists, --target is rejected ("can not be
# provided when tag already exists"). Use --target only on a fresh
# tag; attach to an existing tag without it.
if [ "${TAG_EXISTS}" = "true" ]; then
TARGET_ARG=""
else
TARGET_ARG="--target ${GITHUB_SHA}"
fi
# Idempotent create: tolerate "release already exists" / "tag already
# exists" errors that can happen if a concurrent run or manual action
# created the Release between our existence check and this step.
set +e
OUT="$(gh release create "v${VERSION}" \
--repo "${GITHUB_REPOSITORY}" \
--title "v${VERSION}" \
--generate-notes \
${TARGET_ARG} 2>&1)"
STATUS=$?
set -e
if [ "${STATUS}" -eq 0 ]; then
echo "::notice::Created Release v${VERSION} at ${GITHUB_SHA}. publish.yml will fire on the 'release: published' event."
exit 0
fi
# Recognize the benign "already exists" outcomes and treat them as
# success. Anything else: bail loud with the original error.
if echo "${OUT}" | grep -qiE "release.*already.*exists|already_exists|HTTP 422.*Validation Failed"; then
# Re-confirm via the API rather than trusting the error string.
if gh release view "v${VERSION}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "::notice::Release v${VERSION} already exists (created concurrently or by a previous partial run). Treating as success."
exit 0
fi
fi
# Unrecognized failure mode. Surface the gh CLI output and fail.
echo "::error::gh release create failed for v${VERSION}:"
echo "${OUT}"
exit ${STATUS}
- name: Summarize
if: always()
env:
VERSION: ${{ steps.detect.outputs.version }}
SHOULD_RELEASE: ${{ steps.detect.outputs.should_release }}
ALREADY_EXISTS: ${{ steps.existence.outputs.exists }}
run: |
if [ "${SHOULD_RELEASE}" != "true" ]; then
echo "No version bump → nothing to do."
exit 0
fi
if [ "${ALREADY_EXISTS}" = "true" ]; then
echo "Release v${VERSION} already existed before this run → no-op."
exit 0
fi
echo "Created Release v${VERSION}. publish.yml takes over from here."
publish:
name: Publish (via release env approval gate)
needs: detect-and-release
if: needs.detect-and-release.outputs.should_release == 'true'
uses: ./.github/workflows/publish.yml
with:
tag: ${{ needs.detect-and-release.outputs.tag }}
secrets: inherit
permissions:
contents: read
id-token: write # required for OIDC publishing (cascades to the called workflow)
actions: read # required for the called workflow's approval job to read the approvals API