Skip to content

Commit 4b3596d

Browse files
authored
fix: resolve server release versions from remote tags (#1940)
Resolve server-v3 and server-v4 release versions from remote tags, fail instead of bootstrapping from 0.0.0, and refuse to publish any version that does not advance the highest existing remote tag. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Resolve release versions for `@browserbasehq/stagehand-server-v3` and `@browserbasehq/stagehand-server-v4` from remote Git tags and add guardrails to stop non-advancing or bootstrapped releases. Workflows fail if no remote release tag exists. - **Bug Fixes** - Determine latest remote tag with `git ls-remote --tags` (no local tag reliance). - Fetch the resolved tag before diffing and ignore deleted changeset files. - Require the computed version to advance the highest remote version; otherwise fail. - Fail when no remote tag is found (no `0.0.0` bootstrap). <sup>Written for commit 77c176c. Summary will update on new commits. <a href="https://cubic.dev/pr/browserbase/stagehand/pull/1940">Review in cubic</a></sup> <!-- End of auto-generated description by cubic. -->
1 parent d55aedd commit 4b3596d

2 files changed

Lines changed: 184 additions & 16 deletions

File tree

.github/workflows/stagehand-server-v3-release.yml

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,72 @@ jobs:
4444
run: |
4545
set -euo pipefail
4646
47-
latest_tag="$(git tag -l 'stagehand-server-v3/v*' --sort=-v:refname | head -n 1 || true)"
47+
remote_tags="$(mktemp)"
48+
git ls-remote --tags --refs origin 'refs/tags/stagehand-server-v3/v*' \
49+
| awk '{sub("refs/tags/", "", $2); print $2}' \
50+
> "${remote_tags}"
51+
52+
latest_tag="$(
53+
REMOTE_TAGS_FILE="${remote_tags}" node <<'NODE'
54+
const fs = require('fs');
55+
56+
const tags = fs
57+
.readFileSync(process.env.REMOTE_TAGS_FILE, 'utf8')
58+
.split('\n')
59+
.map((line) => line.trim())
60+
.filter(Boolean);
61+
62+
const parseTag = (tag) => {
63+
const match = /^stagehand-server-v3\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
64+
if (!match) {
65+
return null;
66+
}
67+
return match.slice(1).map((part) => Number(part));
68+
};
69+
70+
const compareVersions = (left, right) => {
71+
for (let i = 0; i < 3; i += 1) {
72+
if (left[i] !== right[i]) {
73+
return left[i] - right[i];
74+
}
75+
}
76+
return 0;
77+
};
78+
79+
let best = null;
80+
for (const tag of tags) {
81+
const version = parseTag(tag);
82+
if (!version) {
83+
continue;
84+
}
85+
if (best === null || compareVersions(version, best.version) > 0) {
86+
best = { tag, version };
87+
}
88+
}
89+
90+
process.stdout.write(best?.tag ?? '');
91+
NODE
92+
)"
93+
4894
changed_files="$(mktemp)"
4995
if [ -n "${latest_tag}" ]; then
96+
git fetch --force origin "refs/tags/${latest_tag}:refs/tags/${latest_tag}"
5097
git diff --diff-filter=d --name-only "${latest_tag}"..HEAD -- '.changeset/*.md' > "${changed_files}"
5198
else
5299
find .changeset -maxdepth 1 -name '*.md' -print | sort > "${changed_files}"
53100
fi
54101
55-
LATEST_TAG="${latest_tag}" CHANGED_CHANGESET_FILES="${changed_files}" node <<'NODE'
102+
LATEST_TAG="${latest_tag}" REMOTE_TAGS_FILE="${remote_tags}" CHANGED_CHANGESET_FILES="${changed_files}" node <<'NODE'
56103
const fs = require('fs');
57104
const path = require('path');
58105
59106
const packageName = '@browserbasehq/stagehand-server-v3';
60107
const latestTag = process.env.LATEST_TAG || '';
108+
const remoteTags = fs
109+
.readFileSync(process.env.REMOTE_TAGS_FILE, 'utf8')
110+
.split('\n')
111+
.map((line) => line.trim())
112+
.filter(Boolean);
61113
const changesetFiles = fs
62114
.readFileSync(process.env.CHANGED_CHANGESET_FILES, 'utf8')
63115
.split('\n')
@@ -66,6 +118,33 @@ jobs:
66118
.filter((file) => path.basename(file) !== 'config.json');
67119
68120
const releasePriority = { patch: 0, minor: 1, major: 2 };
121+
const compareVersions = (left, right) => {
122+
for (let i = 0; i < 3; i += 1) {
123+
if (left[i] !== right[i]) {
124+
return left[i] - right[i];
125+
}
126+
}
127+
return 0;
128+
};
129+
const parseTag = (tag) => {
130+
const match = /^stagehand-server-v3\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
131+
if (!match) {
132+
return null;
133+
}
134+
return match.slice(1).map((part) => Number(part));
135+
};
136+
137+
let highestRemoteVersion = null;
138+
for (const tag of remoteTags) {
139+
const version = parseTag(tag);
140+
if (!version) {
141+
continue;
142+
}
143+
if (highestRemoteVersion === null || compareVersions(version, highestRemoteVersion) > 0) {
144+
highestRemoteVersion = version;
145+
}
146+
}
147+
69148
let highestReleaseType = null;
70149
71150
for (const file of changesetFiles) {
@@ -104,14 +183,13 @@ jobs:
104183
let version = '';
105184
106185
if (shouldRelease) {
107-
const baseVersion = latestTag
108-
? latestTag.replace(/^stagehand-server-v3\/v/, '')
109-
: '0.0.0';
110-
const parts = baseVersion.split('.').map((part) => Number(part));
111-
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
112-
throw new Error(`Invalid latest stagehand-server-v3 tag: ${latestTag || '<none>'}`);
186+
if (!latestTag || highestRemoteVersion === null) {
187+
throw new Error(
188+
'No existing remote stagehand-server-v3 release tag was found. Refusing to bootstrap from 0.0.0.',
189+
);
113190
}
114191
192+
const parts = [...highestRemoteVersion];
115193
if (highestReleaseType === 'major') {
116194
parts[0] += 1;
117195
parts[1] = 0;
@@ -123,6 +201,12 @@ jobs:
123201
parts[2] += 1;
124202
}
125203
204+
if (compareVersions(parts, highestRemoteVersion) <= 0) {
205+
throw new Error(
206+
`Computed stagehand-server-v3 version ${parts.join('.')} does not advance remote latest ${highestRemoteVersion.join('.')}.`,
207+
);
208+
}
209+
126210
version = parts.join('.');
127211
}
128212

.github/workflows/stagehand-server-v4-release.yml

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,72 @@ jobs:
4444
run: |
4545
set -euo pipefail
4646
47-
latest_tag="$(git tag -l 'stagehand-server-v4/v*' --sort=-v:refname | head -n 1 || true)"
47+
remote_tags="$(mktemp)"
48+
git ls-remote --tags --refs origin 'refs/tags/stagehand-server-v4/v*' \
49+
| awk '{sub("refs/tags/", "", $2); print $2}' \
50+
> "${remote_tags}"
51+
52+
latest_tag="$(
53+
REMOTE_TAGS_FILE="${remote_tags}" node <<'NODE'
54+
const fs = require('fs');
55+
56+
const tags = fs
57+
.readFileSync(process.env.REMOTE_TAGS_FILE, 'utf8')
58+
.split('\n')
59+
.map((line) => line.trim())
60+
.filter(Boolean);
61+
62+
const parseTag = (tag) => {
63+
const match = /^stagehand-server-v4\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
64+
if (!match) {
65+
return null;
66+
}
67+
return match.slice(1).map((part) => Number(part));
68+
};
69+
70+
const compareVersions = (left, right) => {
71+
for (let i = 0; i < 3; i += 1) {
72+
if (left[i] !== right[i]) {
73+
return left[i] - right[i];
74+
}
75+
}
76+
return 0;
77+
};
78+
79+
let best = null;
80+
for (const tag of tags) {
81+
const version = parseTag(tag);
82+
if (!version) {
83+
continue;
84+
}
85+
if (best === null || compareVersions(version, best.version) > 0) {
86+
best = { tag, version };
87+
}
88+
}
89+
90+
process.stdout.write(best?.tag ?? '');
91+
NODE
92+
)"
93+
4894
changed_files="$(mktemp)"
4995
if [ -n "${latest_tag}" ]; then
96+
git fetch --force origin "refs/tags/${latest_tag}:refs/tags/${latest_tag}"
5097
git diff --diff-filter=d --name-only "${latest_tag}"..HEAD -- '.changeset/*.md' > "${changed_files}"
5198
else
5299
find .changeset -maxdepth 1 -name '*.md' -print | sort > "${changed_files}"
53100
fi
54101
55-
LATEST_TAG="${latest_tag}" CHANGED_CHANGESET_FILES="${changed_files}" node <<'NODE'
102+
LATEST_TAG="${latest_tag}" REMOTE_TAGS_FILE="${remote_tags}" CHANGED_CHANGESET_FILES="${changed_files}" node <<'NODE'
56103
const fs = require('fs');
57104
const path = require('path');
58105
59106
const packageName = '@browserbasehq/stagehand-server-v4';
60107
const latestTag = process.env.LATEST_TAG || '';
108+
const remoteTags = fs
109+
.readFileSync(process.env.REMOTE_TAGS_FILE, 'utf8')
110+
.split('\n')
111+
.map((line) => line.trim())
112+
.filter(Boolean);
61113
const changesetFiles = fs
62114
.readFileSync(process.env.CHANGED_CHANGESET_FILES, 'utf8')
63115
.split('\n')
@@ -66,6 +118,33 @@ jobs:
66118
.filter((file) => path.basename(file) !== 'config.json');
67119
68120
const releasePriority = { patch: 0, minor: 1, major: 2 };
121+
const compareVersions = (left, right) => {
122+
for (let i = 0; i < 3; i += 1) {
123+
if (left[i] !== right[i]) {
124+
return left[i] - right[i];
125+
}
126+
}
127+
return 0;
128+
};
129+
const parseTag = (tag) => {
130+
const match = /^stagehand-server-v4\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
131+
if (!match) {
132+
return null;
133+
}
134+
return match.slice(1).map((part) => Number(part));
135+
};
136+
137+
let highestRemoteVersion = null;
138+
for (const tag of remoteTags) {
139+
const version = parseTag(tag);
140+
if (!version) {
141+
continue;
142+
}
143+
if (highestRemoteVersion === null || compareVersions(version, highestRemoteVersion) > 0) {
144+
highestRemoteVersion = version;
145+
}
146+
}
147+
69148
let highestReleaseType = null;
70149
71150
for (const file of changesetFiles) {
@@ -104,14 +183,13 @@ jobs:
104183
let version = '';
105184
106185
if (shouldRelease) {
107-
const baseVersion = latestTag
108-
? latestTag.replace(/^stagehand-server-v4\/v/, '')
109-
: '0.0.0';
110-
const parts = baseVersion.split('.').map((part) => Number(part));
111-
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
112-
throw new Error(`Invalid latest stagehand-server-v4 tag: ${latestTag || '<none>'}`);
186+
if (!latestTag || highestRemoteVersion === null) {
187+
throw new Error(
188+
'No existing remote stagehand-server-v4 release tag was found. Refusing to bootstrap from 0.0.0.',
189+
);
113190
}
114191
192+
const parts = [...highestRemoteVersion];
115193
if (highestReleaseType === 'major') {
116194
parts[0] += 1;
117195
parts[1] = 0;
@@ -123,6 +201,12 @@ jobs:
123201
parts[2] += 1;
124202
}
125203
204+
if (compareVersions(parts, highestRemoteVersion) <= 0) {
205+
throw new Error(
206+
`Computed stagehand-server-v4 version ${parts.join('.')} does not advance remote latest ${highestRemoteVersion.join('.')}.`,
207+
);
208+
}
209+
126210
version = parts.join('.');
127211
}
128212

0 commit comments

Comments
 (0)