@@ -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