Skip to content

Commit 62f3c4c

Browse files
authored
feat: Add fast-fail support in github action if it can't retrieve the package-info.json file (#314)
1 parent 197b44e commit 62f3c4c

File tree

9 files changed

+525
-22
lines changed

9 files changed

+525
-22
lines changed

action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ inputs:
2727
description: Optional target repository where to publish releases.
2828
required: false
2929
default: ${{ github.repository }}
30+
require_existing_jdeploy_tag:
31+
description: |
32+
Fail if jdeploy tag doesn't exist (strict mode for established projects).
33+
Set to 'true' for projects that should always have a jdeploy tag.
34+
This prevents accidental data loss by ensuring package-info.json exists before updating.
35+
required: false
36+
default: 'false'
3037

3138
runs:
3239
using: composite
@@ -202,6 +209,7 @@ runs:
202209
GITHUB_REPOSITORY=${{ inputs.target_repository }} $jdeploy_exec github-prepare-release
203210
env:
204211
GH_TOKEN: ${{ github.actor }}:${{ inputs.github_token }}
212+
JDEPLOY_REQUIRE_EXISTING_TAG: ${{ inputs.require_existing_jdeploy_tag }}
205213

206214
- name: Prepare Installer Bundles for Tag
207215
if: ${{ env.JDEPLOY_SKIP_EXECUTION != 'true' && inputs.deploy_target == 'github' && github.ref_type == 'tag' }}
@@ -218,6 +226,7 @@ runs:
218226
GITHUB_REPOSITORY=${{ inputs.target_repository }} $jdeploy_exec github-prepare-release
219227
env:
220228
GH_TOKEN: ${{ github.actor }}:${{ inputs.github_token }}
229+
JDEPLOY_REQUIRE_EXISTING_TAG: ${{ inputs.require_existing_jdeploy_tag }}
221230

222231
- name: Publish package-info.json to GitHub
223232
uses: softprops/action-gh-release@v2

cli/src/main/java/ca/weblite/jdeploy/publishing/github/GitHubPublishDriver.java

Lines changed: 164 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ca.weblite.jdeploy.appbundler.BundlerSettings;
44
import ca.weblite.jdeploy.downloadPage.DownloadPageSettings;
55
import ca.weblite.jdeploy.downloadPage.DownloadPageSettingsService;
6+
import ca.weblite.jdeploy.environment.Environment;
67
import ca.weblite.jdeploy.factories.CheerpjServiceFactory;
78
import ca.weblite.jdeploy.helpers.GithubReleaseNotesMutator;
89
import ca.weblite.jdeploy.helpers.PackageInfoBuilder;
@@ -27,6 +28,8 @@
2728
import org.apache.commons.io.FileUtils;
2829
import org.json.JSONObject;
2930

31+
import java.nio.charset.StandardCharsets;
32+
3033
import javax.inject.Inject;
3134
import javax.inject.Singleton;
3235
import 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

cli/src/main/resources/ca/weblite/jdeploy/github/ant-workflow.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ on:
77
branches: [ '*' ]
88
tags: [ '*' ]
99

10+
# Prevent concurrent jDeploy workflows to avoid race conditions
11+
concurrency:
12+
group: jdeploy-${{ github.repository }}
13+
cancel-in-progress: false
14+
1015
jobs:
1116
build:
1217
permissions:

cli/src/main/resources/ca/weblite/jdeploy/github/gradle-workflow.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ on:
88
branches: [ '*' ]
99
tags: [ '*' ]
1010

11+
# Prevent concurrent jDeploy workflows to avoid race conditions
12+
concurrency:
13+
group: jdeploy-${{ github.repository }}
14+
cancel-in-progress: false
15+
1116
permissions:
1217
contents: read
1318

cli/src/main/resources/ca/weblite/jdeploy/github/maven-workflow.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ on:
88
branches: ['*']
99
tags: ['*']
1010

11+
# Prevent concurrent jDeploy workflows to avoid race conditions
12+
concurrency:
13+
group: jdeploy-${{ github.repository }}
14+
cancel-in-progress: false
15+
1116
jobs:
1217
build:
1318
permissions:

cli/src/test/java/ca/weblite/jdeploy/publishing/github/GitHubPublishDriverIntegrationTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ public void setup() throws Exception {
140140
ca.weblite.jdeploy.services.PlatformBundleGenerator platformBundleGenerator = mock(ca.weblite.jdeploy.services.PlatformBundleGenerator.class);
141141
ca.weblite.jdeploy.services.DefaultBundleService defaultBundleService = mock(ca.weblite.jdeploy.services.DefaultBundleService.class);
142142
ca.weblite.jdeploy.factories.JDeployProjectFactory projectFactory = mock(ca.weblite.jdeploy.factories.JDeployProjectFactory.class);
143-
143+
ca.weblite.jdeploy.environment.Environment githubDriverEnvironment = mock(ca.weblite.jdeploy.environment.Environment.class);
144+
144145
when(cheerpjServiceFactory.create(any())).thenReturn(cheerpjService);
145146
when(cheerpjService.isEnabled()).thenReturn(false);
146147
when(packageNameService.getFullPackageName(any(), any())).thenReturn("test-app");
147-
148+
148149
githubDriver = new GitHubPublishDriver(
149150
baseDriver,
150151
bundleCodeService,
@@ -154,7 +155,8 @@ public void setup() throws Exception {
154155
downloadPageSettingsService,
155156
platformBundleGenerator,
156157
defaultBundleService,
157-
projectFactory
158+
projectFactory,
159+
githubDriverEnvironment
158160
);
159161

160162
// Setup test target

0 commit comments

Comments
 (0)