33import ca .weblite .jdeploy .appbundler .BundlerSettings ;
44import ca .weblite .jdeploy .downloadPage .DownloadPageSettings ;
55import ca .weblite .jdeploy .downloadPage .DownloadPageSettingsService ;
6+ import ca .weblite .jdeploy .environment .Environment ;
67import ca .weblite .jdeploy .factories .CheerpjServiceFactory ;
78import ca .weblite .jdeploy .helpers .GithubReleaseNotesMutator ;
89import ca .weblite .jdeploy .helpers .PackageInfoBuilder ;
2728import org .apache .commons .io .FileUtils ;
2829import org .json .JSONObject ;
2930
31+ import java .nio .charset .StandardCharsets ;
32+
3033import javax .inject .Inject ;
3134import javax .inject .Singleton ;
3235import java .io .*;
@@ -58,11 +61,13 @@ public class GitHubPublishDriver implements PublishDriverInterface {
5861 private final DownloadPageSettingsService downloadPageSettingsService ;
5962
6063 private final PlatformBundleGenerator platformBundleGenerator ;
61-
64+
6265 private final DefaultBundleService defaultBundleService ;
63-
66+
6467 private final JDeployProjectFactory projectFactory ;
6568
69+ private final Environment environment ;
70+
6671 @ Inject
6772 public GitHubPublishDriver (
6873 BasePublishDriver baseDriver ,
@@ -73,7 +78,8 @@ public GitHubPublishDriver(
7378 DownloadPageSettingsService downloadPageSettingsService ,
7479 PlatformBundleGenerator platformBundleGenerator ,
7580 DefaultBundleService defaultBundleService ,
76- JDeployProjectFactory projectFactory
81+ JDeployProjectFactory projectFactory ,
82+ Environment environment
7783 ) {
7884 this .baseDriver = baseDriver ;
7985 this .bundleCodeService = bundleCodeService ;
@@ -84,6 +90,7 @@ public GitHubPublishDriver(
8490 this .platformBundleGenerator = platformBundleGenerator ;
8591 this .defaultBundleService = defaultBundleService ;
8692 this .projectFactory = projectFactory ;
93+ this .environment = environment ;
8794 }
8895
8996 @ Override
@@ -165,10 +172,22 @@ public void prepare(
165172
166173 saveGithubReleaseFiles (context , target );
167174 PackageInfoBuilder builder = new PackageInfoBuilder ();
168- InputStream oldPackageInfo = loadPackageInfo (context , target );
175+ InputStream oldPackageInfo = loadPackageInfo (context , target ); // May throw IOException now
169176 if (oldPackageInfo != null ) {
170- builder .load (oldPackageInfo );
177+ try {
178+ builder .load (oldPackageInfo );
179+ context .out ().println ("Loaded existing package-info.json with version history" );
180+ } catch (Exception ex ) {
181+ throw new IOException (
182+ "CRITICAL: Failed to parse existing package-info.json. " +
183+ "The file exists but is corrupted or invalid. " +
184+ "Cannot proceed as this would cause data loss." ,
185+ ex
186+ );
187+ }
171188 } else {
189+ // Only reached if jdeploy tag doesn't exist and strict mode is off
190+ context .out ().println ("Creating new package-info.json for first release" );
172191 builder .setCreatedTime ();
173192 }
174193 builder .setModifiedTime ();
@@ -178,10 +197,12 @@ public void prepare(
178197 if (!PrereleaseHelper .isPrereleaseVersion (version )) {
179198 builder .setLatestVersion (version );
180199 }
181- builder .save (
182- Files .newOutputStream (new File (context .getGithubReleaseFilesDir (),
183- "package-info.json" ).toPath ())
184- );
200+
201+ File packageInfoFile = new File (context .getGithubReleaseFilesDir (), "package-info.json" );
202+ builder .save (Files .newOutputStream (packageInfoFile .toPath ()));
203+
204+ // Verify the saved file
205+ verifyPackageInfoIntegrity (context , packageInfoFile , version , oldPackageInfo != null );
185206 // Trigger register of package name
186207
187208 bundleCodeService .fetchJdeployBundleCode (
@@ -298,16 +319,144 @@ private void saveGithubReleaseFiles(PublishingContext context, PublishTargetInte
298319 context .out ().println ("Assets copied to " + releaseFilesDir );
299320 }
300321
301- private InputStream loadPackageInfo (PublishingContext context , PublishTargetInterface target ) {
322+ private InputStream loadPackageInfo (PublishingContext context , PublishTargetInterface target ) throws IOException {
302323 String packageInfoUrl = target .getUrl () + "/releases/download/jdeploy/package-info.json" ;
324+
325+ // First check if jdeploy tag exists
326+ boolean jdeployTagExists = checkJdeployTagExists (context , target );
327+
328+ int maxRetries = 3 ;
329+ int retryDelayMs = 2000 ;
330+ IOException lastException = null ;
331+
332+ for (int attempt = 1 ; attempt <= maxRetries ; attempt ++) {
333+ try {
334+ InputStream stream = URLUtil .openStream (new URL (packageInfoUrl ));
335+ // Validate that we can parse it as JSON
336+ validatePackageInfoJson (stream );
337+ // Re-open stream since validation consumed it
338+ return URLUtil .openStream (new URL (packageInfoUrl ));
339+ } catch (IOException ex ) {
340+ lastException = ex ;
341+ if (attempt < maxRetries ) {
342+ context .out ().println (
343+ "Attempt " + attempt + "/" + maxRetries +
344+ " failed to load package-info.json from " + packageInfoUrl +
345+ ", retrying in " + retryDelayMs + "ms... Error: " + ex .getMessage ()
346+ );
347+ try {
348+ Thread .sleep (retryDelayMs );
349+ retryDelayMs *= 2 ; // Exponential backoff
350+ } catch (InterruptedException ie ) {
351+ Thread .currentThread ().interrupt ();
352+ throw new IOException ("Interrupted while retrying package-info.json load" , ie );
353+ }
354+ }
355+ }
356+ }
357+
358+ // All retries exhausted
359+ if (jdeployTagExists ) {
360+ // jdeploy tag exists but we can't load package-info.json - CRITICAL ERROR
361+ throw new IOException (
362+ "CRITICAL: The 'jdeploy' tag exists in the repository but package-info.json " +
363+ "could not be retrieved or parsed after " + maxRetries + " attempts at " +
364+ packageInfoUrl + ". This indicates a serious issue. " +
365+ "Cannot proceed as this would cause data loss. " +
366+ "Please investigate the jdeploy release and its assets. " +
367+ "Last error: " + (lastException != null ? lastException .getMessage () : "unknown" ),
368+ lastException
369+ );
370+ }
371+
372+ // Check if strict mode is enabled (require jdeploy tag to exist)
373+ boolean requireExistingTag = "true" .equals (environment .get ("JDEPLOY_REQUIRE_EXISTING_TAG" ));
374+ if (requireExistingTag ) {
375+ throw new IOException (
376+ "CRITICAL: jdeploy tag does not exist but JDEPLOY_REQUIRE_EXISTING_TAG is set to true. " +
377+ "This project requires an existing jdeploy tag. " +
378+ "If this is the first release, set require_existing_jdeploy_tag: 'false' in your workflow." ,
379+ lastException
380+ );
381+ }
382+
383+ // jdeploy tag doesn't exist - this is likely the first release
384+ context .out ().println (
385+ "jdeploy tag not found at " + packageInfoUrl +
386+ ". This appears to be the first release, creating new package-info.json"
387+ );
388+ return null ;
389+ }
390+
391+ private boolean checkJdeployTagExists (PublishingContext context , PublishTargetInterface target ) {
303392 try {
304- return URLUtil .openStream (new URL (packageInfoUrl ));
305- } catch (IOException ex ) {
393+ String tagsUrl = target .getUrl () + "/releases/tags/jdeploy" ;
394+ HttpURLConnection conn = (HttpURLConnection ) new URL (tagsUrl ).openConnection ();
395+ conn .setRequestMethod ("GET" );
396+ conn .setInstanceFollowRedirects (true );
397+ conn .setConnectTimeout (5000 );
398+ conn .setReadTimeout (5000 );
399+
400+ int responseCode = conn .getResponseCode ();
401+ conn .disconnect ();
402+
403+ return responseCode == 200 ;
404+ } catch (Exception ex ) {
306405 context .out ().println (
307- "Failed to open stream for existing package-info.json at " + packageInfoUrl +
308- ". Perhaps it doesn't exist yet"
406+ "Could not check if jdeploy tag exists (assuming it doesn't): " + ex .getMessage ()
407+ );
408+ return false ;
409+ }
410+ }
411+
412+ private void validatePackageInfoJson (InputStream stream ) throws IOException {
413+ try {
414+ String content = IOUtil .readToString (stream );
415+ JSONObject json = new JSONObject (content );
416+
417+ // Validate required fields
418+ if (!json .has ("name" )) {
419+ throw new IOException ("package-info.json is missing required 'name' field" );
420+ }
421+ if (!json .has ("versions" )) {
422+ throw new IOException ("package-info.json is missing required 'versions' field" );
423+ }
424+
425+ // Basic structure validation
426+ json .getJSONObject ("versions" ); // Will throw if not an object
427+
428+ } catch (org .json .JSONException ex ) {
429+ throw new IOException ("package-info.json is not valid JSON: " + ex .getMessage (), ex );
430+ }
431+ }
432+
433+ private void verifyPackageInfoIntegrity (
434+ PublishingContext context ,
435+ File packageInfoFile ,
436+ String currentVersion ,
437+ boolean hadPreviousVersions
438+ ) throws IOException {
439+ try {
440+ String content = FileUtils .readFileToString (packageInfoFile , StandardCharsets .UTF_8 );
441+ JSONObject json = new JSONObject (content );
442+
443+ // Verify current version was added
444+ if (!json .getJSONObject ("versions" ).has (currentVersion )) {
445+ throw new IOException (
446+ "Verification failed: package-info.json is missing the current version " + currentVersion
447+ );
448+ }
449+
450+ context .out ().println (
451+ "Verification passed: package-info.json contains " +
452+ json .getJSONObject ("versions" ).length () + " version(s)"
453+ );
454+
455+ } catch (org .json .JSONException ex ) {
456+ throw new IOException (
457+ "Verification failed: Generated package-info.json is invalid JSON" ,
458+ ex
309459 );
310- return null ;
311460 }
312461 }
313462
0 commit comments