Skip to content

Commit 061a7e3

Browse files
authored
fix: update_cucumber script should not fail on manually created releases (#396)
The script was assuming that `target_commitish` in the releases API would generally be the hash of the release tag, other than for an edge case on v29.0.0 which had been patched manually in the code. In fact, `target_commitish` will only be a hash if the tag was created before the release (e.g. by cucumber's automated release actions). If the release is created manually in the github UI with a not-yet-existing tag then `target_commitish` will instead be the branch name it was created from. In that case, the only way to get the hash is to separately look up the tag name in the tags API. This was causing the script to fail due to two recent manual releases on the upstream repo. I have fixed this to now look up tag details if they're missing from the releases API. As part of this, I have refactored to only look up releases that are `>=` the one we are currently pinned to. This means we will only need to look up tags for manual releases until we update past them.
1 parent 9dec914 commit 061a7e3

File tree

1 file changed

+83
-17
lines changed

1 file changed

+83
-17
lines changed

bin/update_cucumber

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class {
3636
$currentVersion = $this->findComposerGherkinVersion($composerFile);
3737
echo "Current local version is {$currentVersion['hash']} (tagged {$currentVersion['tag_name']})\n";
3838

39-
$releases = $this->listGitHubReleases('cucumber/gherkin');
39+
$releases = $this->listGitHubReleasesSince('cucumber/gherkin', $currentVersion['tag_name']);
4040
echo "Latest upstream version is {$releases[0]['hash']} (tagged {$releases[0]['tag_name']})\n";
4141

4242
// We only want to bump by one version at a time so that any test failures are easier to trace
@@ -112,14 +112,12 @@ class {
112112
/**
113113
* @phpstan-return non-empty-list<TGitHubReleaseArray>
114114
*/
115-
private function listGitHubReleases(string $repo): array
115+
private function listGitHubReleasesSince(string $repo, string $currentVersionTag): array
116116
{
117117
// GitHub requires API requests to send a user_agent header for tracing
118118
ini_set('user_agent', 'https://github.com/Behat/Gherkin updater');
119119
$releases = Behat\Gherkin\Filesystem::readJsonFileArray('https://api.github.com/repos/' . $repo . '/releases');
120120

121-
assert($releases !== [], 'github should have returned at least one release');
122-
123121
// Sanity check and simplify the GitHub API response structure to reduce assertions and type issues elsewhere
124122
$releases = array_map(
125123
static function (mixed $release) {
@@ -138,19 +136,8 @@ class {
138136
'github release JSON should match expected structure',
139137
);
140138

141-
// Edge case: they created the 29.0.0 release manually and for some reason it reports a target_commitish
142-
// of `main` rather than the hash of the v29.0.0 tag. We could look up the tag in the API, but not worth
143-
// it just for this one so hardcoded the value instead.
144-
if ($release['tag_name'] === 'v29.0.0') {
145-
$release['target_commitish'] = 'c7d527d49d67ae39fe5a91aabcc5578c23ef1c5c';
146-
}
147-
148-
// Protect against any future releases that don't properly target a tag SHA.
149-
assert(
150-
(bool) preg_match('/^[a-z0-9]{40}$/', $release['target_commitish']),
151-
'target_commitish for ' . $release['tag_name'] . ' should be a hash',
152-
);
153-
139+
// Note: `target_commitish` may not actually be a hash, but we will look this up later once we know if
140+
// we actually need this release. See populateMissingTagHashes below.
154141
return [
155142
'tag_name' => $release['tag_name'],
156143
'hash' => $release['target_commitish'],
@@ -161,13 +148,92 @@ class {
161148
$releases,
162149
);
163150

151+
// Remove older releases - we only need to validate the current release & possibly bump forwards.
152+
$releases = array_values(array_filter(
153+
$releases,
154+
static fn (array $release) => version_compare($release['tag_name'], $currentVersionTag, '>=')
155+
));
156+
assert($releases !== [], 'Should have found at least one current or newer release');
157+
158+
// The github releases API only contains a tag/commit hash for releases that were created from an existing tag.
159+
// We may need to look up hashes for releases where the release & tag were created manually from a branch.
160+
$releases = $this->populateMissingTagHashes($repo, $releases);
161+
164162
// Releases API is in order of github release ID, this may not be version order (e.g. major releases may be
165163
// interspersed with point releases to older versions). So sort it most recent -> oldest by version number.
166164
usort($releases, static fn (array $a, array $b) => version_compare($b['tag_name'], $a['tag_name']));
167165

168166
return $releases;
169167
}
170168

169+
/**
170+
* @phpstan-param non-empty-list<TGitHubReleaseArray> $releases
171+
*
172+
* @phpstan-return non-empty-list<TGitHubReleaseArray>
173+
*/
174+
private function populateMissingTagHashes(string $repo, array $releases): array
175+
{
176+
if (array_filter($releases, $this->hasValidHash(...)) === $releases) {
177+
// All releases we're interested in have a hash already, nothing to do
178+
return $releases;
179+
}
180+
181+
echo "Some releases do not have hashes, looking up tags\n";
182+
$tags = Behat\Gherkin\Filesystem::readJsonFileArray('https://api.github.com/repos/' . $repo . '/tags');
183+
$tagMap = [];
184+
foreach ($tags as $tag) {
185+
assert(
186+
is_array($tag)
187+
&& is_string($tag['name'] ?? null)
188+
&& is_array($tag['commit'] ?? null)
189+
&& is_string($tag['commit']['sha'] ?? null),
190+
'github tag JSON should match expected structure',
191+
);
192+
$tagMap[$tag['name']] = $tag['commit']['sha'];
193+
}
194+
195+
return array_map(
196+
function (array $release) use ($tagMap) {
197+
if ($this->hasValidHash($release)) {
198+
return $release;
199+
}
200+
201+
$originalHash = $release['hash'];
202+
$tagHash = $tagMap[$release['tag_name']] ?? null;
203+
if ($tagHash !== null) {
204+
$release['hash'] = $tagHash;
205+
}
206+
207+
assert(
208+
$this->hasValidHash($release),
209+
sprintf(
210+
'github tags API should provide valid hash for %s with release target of %s',
211+
$release['tag_name'],
212+
$release['hash']
213+
)
214+
);
215+
216+
echo sprintf(
217+
" - Identified %s (target: %s) as %s\n",
218+
$release['tag_name'],
219+
$originalHash,
220+
$release['hash']
221+
);
222+
223+
return $release;
224+
},
225+
$releases,
226+
);
227+
}
228+
229+
/**
230+
* @param TGitHubReleaseArray $release
231+
*/
232+
private function hasValidHash(array $release): bool
233+
{
234+
return preg_match('/^[a-z0-9]{40}$/', $release['hash']) === 1;
235+
}
236+
171237
/**
172238
* @phpstan-param non-empty-list<TGitHubReleaseArray> $releases
173239
* @phpstan-param TCurrentVersionArray $currentVersion

0 commit comments

Comments
 (0)