diff --git a/cli/src/main/java/ca/weblite/jdeploy/builders/ProjectGeneratorRequestBuilder.java b/cli/src/main/java/ca/weblite/jdeploy/builders/ProjectGeneratorRequestBuilder.java index e0829d23..656ce08a 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/builders/ProjectGeneratorRequestBuilder.java +++ b/cli/src/main/java/ca/weblite/jdeploy/builders/ProjectGeneratorRequestBuilder.java @@ -19,6 +19,9 @@ public class ProjectGeneratorRequestBuilder implements ProjectGeneratorRequest.P @CommandLineParser.Help("Whether the repository should be private") private boolean privateRepository; + @CommandLineParser.Help("Whether to create the GitHub repository. If false, the repository is assumed to already exist and is only used as the push target. Defaults to true.") + private boolean createGitHubRepository = true; + @CommandLineParser.Help("Whether to use an existing directory") private boolean useExistingDirectory; @@ -394,6 +397,18 @@ public ProjectGeneratorRequest.Params setPrivateRepository(boolean privateReposi return this; } + @Override + public ProjectGeneratorRequest.Params setCreateGitHubRepository(boolean createGitHubRepository) { + this.createGitHubRepository = createGitHubRepository; + + return this; + } + + @Override + public boolean isCreateGitHubRepository() { + return createGitHubRepository; + } + public String getGithubRepository() { if (githubRepository != null) { return githubRepository; diff --git a/cli/src/main/java/ca/weblite/jdeploy/dtos/ProjectGeneratorRequest.java b/cli/src/main/java/ca/weblite/jdeploy/dtos/ProjectGeneratorRequest.java index 72c3686b..9e50d38d 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/dtos/ProjectGeneratorRequest.java +++ b/cli/src/main/java/ca/weblite/jdeploy/dtos/ProjectGeneratorRequest.java @@ -23,6 +23,8 @@ public class ProjectGeneratorRequest { private final boolean privateRepository; + private final boolean createGitHubRepository; + private final boolean useExistingDirectory; public File getParentDirectory() { @@ -73,6 +75,10 @@ public boolean isPrivateRepository() { return privateRepository; } + public boolean isCreateGitHubRepository() { + return createGitHubRepository; + } + public boolean isUseExistingDirectory() { return useExistingDirectory; } @@ -86,6 +92,9 @@ public interface Params { @CommandLineParser.Alias("p") boolean isPrivateRepository(); + @CommandLineParser.Help("Whether to create the GitHub repository. If false, the repository is assumed to already exist and is only used as the push target. Defaults to true.") + boolean isCreateGitHubRepository(); + @CommandLineParser.Help("Use existing directory") @CommandLineParser.Alias("e") boolean isUseExistingDirectory(); @@ -161,6 +170,8 @@ public interface Params { Params setPrivateRepository(boolean privateRepository); + Params setCreateGitHubRepository(boolean createGitHubRepository); + Params setUseExistingDirectory(boolean useExistingDirectory); Params setExtensions(String[] extensions); @@ -182,6 +193,7 @@ public ProjectGeneratorRequest(Params params) { this.extensions = params.getExtensions(); this.githubRepository = params.getGithubRepository(); this.privateRepository = params.isPrivateRepository(); + this.createGitHubRepository = params.isCreateGitHubRepository(); this.useExistingDirectory = params.isUseExistingDirectory(); } } diff --git a/cli/src/main/java/ca/weblite/jdeploy/services/GitHubAuthenticationException.java b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubAuthenticationException.java new file mode 100644 index 00000000..67a498e6 --- /dev/null +++ b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubAuthenticationException.java @@ -0,0 +1,21 @@ +package ca.weblite.jdeploy.services; + +import java.io.IOException; + +/** + * Thrown when a GitHub API request fails because of invalid, expired or missing + * credentials (HTTP 401). It extends {@link IOException} so existing callers + * continue to work, while allowing callers that care about authentication to + * distinguish a bad token from other I/O errors and prompt the user to + * re-authenticate. + */ +public class GitHubAuthenticationException extends IOException { + + public GitHubAuthenticationException(String message) { + super(message); + } + + public GitHubAuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cli/src/main/java/ca/weblite/jdeploy/services/GitHubRepositoryInitializer.java b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubRepositoryInitializer.java index d6cccf4d..c2422247 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/services/GitHubRepositoryInitializer.java +++ b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubRepositoryInitializer.java @@ -89,17 +89,24 @@ public void createGitHubRepository(GitHubRepositoryIntializationRequest request) "Repository '" + githubRepositoryDto.getFullRepositoryName() + "' created successfully." ); } else { - try (BufferedReader errorReader = new BufferedReader( - new InputStreamReader(connection.getErrorStream())) - ) { - StringBuilder errorMessage = new StringBuilder(); - String line; - while ((line = errorReader.readLine()) != null) { - errorMessage.append(line); + StringBuilder errorMessage = new StringBuilder(); + if (connection.getErrorStream() != null) { + try (BufferedReader errorReader = new BufferedReader( + new InputStreamReader(connection.getErrorStream())) + ) { + String line; + while ((line = errorReader.readLine()) != null) { + errorMessage.append(line); + } } - throw new IOException("Failed to create the repository. " + - "Response code: " + responseCode + ", Error message: " + errorMessage); } + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw new GitHubAuthenticationException("Failed to create the repository. " + + "GitHub rejected the credentials (HTTP 401). Your GitHub token may be " + + "missing, invalid or expired. Error message: " + errorMessage); + } + throw new IOException("Failed to create the repository. " + + "Response code: " + responseCode + ", Error message: " + errorMessage); } } finally { connection.disconnect(); diff --git a/cli/src/main/java/ca/weblite/jdeploy/services/GitHubUsernameService.java b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubUsernameService.java index 4a32aef3..76b2ee0e 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/services/GitHubUsernameService.java +++ b/cli/src/main/java/ca/weblite/jdeploy/services/GitHubUsernameService.java @@ -46,6 +46,10 @@ public String getGitHubUsername() throws IOException { return jsonResponse.getString("login"); } } else { + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw new GitHubAuthenticationException("Failed to authenticate with GitHub (HTTP 401). " + + "Your GitHub token may be missing, invalid or expired."); + } throw new IOException("Failed to retrieve GitHub username. Response code: " + responseCode); } } diff --git a/cli/src/main/java/ca/weblite/jdeploy/services/ProjectGenerator.java b/cli/src/main/java/ca/weblite/jdeploy/services/ProjectGenerator.java index 443e6765..60e1f234 100644 --- a/cli/src/main/java/ca/weblite/jdeploy/services/ProjectGenerator.java +++ b/cli/src/main/java/ca/weblite/jdeploy/services/ProjectGenerator.java @@ -70,52 +70,66 @@ public File generate(ProjectGeneratorRequest request) throws Exception { if (!request.isUseExistingDirectory() && projectDir.exists() ) { throw new Exception("Project directory already exists: " + projectDir.getAbsolutePath()); } + // Track whether we created the directory so we can clean it up if a later + // step fails (e.g. a GitHub authentication error), rather than leaving a + // half-generated project behind that blocks the user from retrying. + boolean projectDirCreated = !request.isUseExistingDirectory(); projectDir.mkdirs(); if (!projectDir.exists()) { throw new IOException("Failed to create project directory: " + projectDir.getAbsolutePath()); } - String templateDirectory = request.getTemplateDirectory(); - if (templateDirectory == null && request.getTemplateName() != null) { - projectTemplateCatalog.update(); - templateDirectory = projectTemplateCatalog.getProjectTemplate(request.getTemplateName()).getAbsolutePath(); - } - if (templateDirectory == null) { - throw new Exception("Template directory is not set"); - } + try { + String templateDirectory = request.getTemplateDirectory(); + if (templateDirectory == null && request.getTemplateName() != null) { + projectTemplateCatalog.update(); + templateDirectory = projectTemplateCatalog.getProjectTemplate(request.getTemplateName()).getAbsolutePath(); + } + if (templateDirectory == null) { + throw new Exception("Template directory is not set"); + } - File templateDir = new File(templateDirectory); - if ( !templateDir.exists() ) { - throw new Exception("Template directory does not exist: " + templateDir.getAbsolutePath()); - } - File[] files = templateDir.listFiles(); - if (files == null) { - throw new IOException("Failed to get files in template directory: " + templateDir.getAbsolutePath()); - } - for ( File file : files) { - if ( file.isDirectory() ) { - FileUtils.copyDirectory(file, new File(projectDir, file.getName())); - } else { - FileUtils.copyFileToDirectory(file, projectDir); + File templateDir = new File(templateDirectory); + if ( !templateDir.exists() ) { + throw new Exception("Template directory does not exist: " + templateDir.getAbsolutePath()); } - } - if (request.getExtensions() != null) { - for (String extension : request.getExtensions()) { - File extensionDir = projectTemplateCatalog.getExtensionTemplate(extension); - applyExtensionToProject(projectDir, extensionDir); + File[] files = templateDir.listFiles(); + if (files == null) { + throw new IOException("Failed to get files in template directory: " + templateDir.getAbsolutePath()); + } + for ( File file : files) { + if ( file.isDirectory() ) { + FileUtils.copyDirectory(file, new File(projectDir, file.getName())); + } else { + FileUtils.copyFileToDirectory(file, projectDir); + } + } + if (request.getExtensions() != null) { + for (String extension : request.getExtensions()) { + File extensionDir = projectTemplateCatalog.getExtensionTemplate(extension); + applyExtensionToProject(projectDir, extensionDir); + } } - } - updateFilesInDirectory(projectDir, request); - updatePackageJsonWithGithubSettings(projectDir, request); + updateFilesInDirectory(projectDir, request); + updatePackageJsonWithGithubSettings(projectDir, request); - if (mavenWrapperInjector.isMavenProject(projectDir.getPath())) { - mavenWrapperInjector.installIntoProject(projectDir.getPath()); - } + if (mavenWrapperInjector.isMavenProject(projectDir.getPath())) { + mavenWrapperInjector.installIntoProject(projectDir.getPath()); + } - initializeAndPushGitRepository(projectDir, request); - return projectDir; + initializeAndPushGitRepository(projectDir, request); + return projectDir; + } catch (Exception e) { + if (projectDirCreated) { + FileUtils.deleteQuietly(projectDir); + FileUtils.deleteQuietly( + new File(projectDir.getParentFile(), projectDir.getName() + "-releases") + ); + } + throw e; + } } @@ -191,7 +205,9 @@ private File initializeAndPushGitRepository( requestBuilder.setRepoName(request.getGithubRepository()) .setProjectPath(projectDirectory.getAbsolutePath()) .setPrivate(request.isPrivateRepository()); - gitHubRepositoryInitializer.createGitHubRepository(requestBuilder.build()); + if (request.isCreateGitHubRepository()) { + gitHubRepositoryInitializer.createGitHubRepository(requestBuilder.build()); + } if (request.isPrivateRepository()) { addSecretToWorkflow(request);