-
Notifications
You must be signed in to change notification settings - Fork 0
Add daemon mode for automatic daily backups with Docker support #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5262e2e
3bca8bc
6301562
ae861dd
a5d3d68
721d02a
c52ad87
b250411
32f772d
0f91b7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # GitHub Personal Access Token (optional but recommended for higher API rate limits) | ||
| # Get one from: https://github.com/settings/tokens | ||
| GITHUB_TOKEN=your_github_token_here | ||
|
|
||
| # Comma-separated list of GitHub users/organizations to backup | ||
| # Example: SCHEDULED_USERS=octocat,github,spring-projects | ||
| SCHEDULED_USERS=octocat |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main, develop ] | ||
| pull_request: | ||
| branches: [ main, develop ] | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Run Unit Tests | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: 'maven' | ||
|
|
||
| - name: Run tests | ||
| run: mvn clean test | ||
|
|
||
| - name: Upload test results | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: test-results | ||
| path: target/surefire-reports/ | ||
| retention-days: 30 | ||
|
|
||
| build: | ||
| name: Build Application | ||
| runs-on: ubuntu-latest | ||
| needs: test | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: 'maven' | ||
|
|
||
| - name: Build with Maven | ||
| run: mvn clean package -DskipTests | ||
|
|
||
| - name: Upload JAR artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: gh-backup-jar | ||
| path: target/*.jar | ||
| retention-days: 30 | ||
|
|
||
| docker-build: | ||
| name: Docker Build Test | ||
| runs-on: ubuntu-latest | ||
| needs: test | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Build Docker image | ||
| uses: docker/build-push-action@v5 | ||
| with: | ||
| context: . | ||
| push: false | ||
| tags: gh-backup:test | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||
| # Build stage | ||||||||||||
| FROM maven:3.9-eclipse-temurin-17 AS build | ||||||||||||
| WORKDIR /app | ||||||||||||
| COPY pom.xml . | ||||||||||||
| COPY src ./src | ||||||||||||
| # Skip tests during Docker build to speed up image creation | ||||||||||||
| # Tests should be run in CI/CD pipeline before building the image | ||||||||||||
| RUN mvn clean package -DskipTests | ||||||||||||
|
|
||||||||||||
| # Runtime stage | ||||||||||||
| FROM eclipse-temurin:17-jre-alpine | ||||||||||||
| WORKDIR /app | ||||||||||||
|
|
||||||||||||
| # Install git (required for cloning repositories) | ||||||||||||
| RUN apk add --no-cache git | ||||||||||||
|
|
||||||||||||
| # Copy the built jar from build stage | ||||||||||||
| COPY --from=build /app/target/gh-backup-1.0.0.jar gh-backup.jar | ||||||||||||
|
|
||||||||||||
| # Create backup directory | ||||||||||||
| RUN mkdir -p /backups | ||||||||||||
|
|
||||||||||||
| # Set default environment variables | ||||||||||||
| ENV BACKUP_DIRECTORY=/backups | ||||||||||||
| ENV GITHUB_TOKEN="" | ||||||||||||
| ENV SCHEDULED_USERS="" | ||||||||||||
|
|
||||||||||||
| # Expose port 8080 - only used if running in web mode with -Dspring.profiles.active=web | ||||||||||||
| # The default daemon mode does not use this port | ||||||||||||
|
Comment on lines
+28
to
+29
|
||||||||||||
| # Expose port 8080 - only used if running in web mode with -Dspring.profiles.active=web | |
| # The default daemon mode does not use this port | |
| # Expose port 8080 for optional web mode. | |
| # The default image entrypoint runs in daemon mode, so this port is only used if the | |
| # container is started in web mode by overriding the default startup behavior. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| services: | ||
| gh-backup-daemon: | ||
| build: . | ||
| container_name: gh-backup-daemon | ||
| restart: unless-stopped | ||
| environment: | ||
| # Set your GitHub personal access token here for higher API rate limits | ||
| - GITHUB_TOKEN=${GITHUB_TOKEN:-} | ||
|
|
||
| # Comma-separated list of GitHub users/organizations to backup | ||
| # Example: SCHEDULED_USERS=octocat,github,spring-projects | ||
| - SCHEDULED_USERS=${SCHEDULED_USERS:-} | ||
|
|
||
| # Optional: Custom backup directory inside container | ||
| - BACKUP_DIRECTORY=/backups | ||
| volumes: | ||
| # Mount a local directory to persist backups | ||
| - ./backups:/backups | ||
| # Keep container running | ||
| stdin_open: true | ||
| tty: true |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/bin/sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Build Java command with environment variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="-Dspring.profiles.active=daemon" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Set backup directory if provided (defaults to /backups in Dockerfile ENV) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$BACKUP_DIRECTORY" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=${BACKUP_DIRECTORY}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Set scheduled users if provided | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$SCHEDULED_USERS" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=${SCHEDULED_USERS}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Execute the application | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exec java $JAVA_OPTS -jar gh-backup.jar "$@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+18
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JAVA_OPTS="-Dspring.profiles.active=daemon" | |
| # Set backup directory if provided (defaults to /backups in Dockerfile ENV) | |
| if [ -n "$BACKUP_DIRECTORY" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.directory=${BACKUP_DIRECTORY}" | |
| fi | |
| # Set scheduled users if provided | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| JAVA_OPTS="$JAVA_OPTS -Dbackup.scheduled.users=${SCHEDULED_USERS}" | |
| fi | |
| # Execute the application | |
| exec java $JAVA_OPTS -jar gh-backup.jar "$@" | |
| set -- java "-Dspring.profiles.active=daemon" | |
| # Set backup directory if provided (defaults to /backups in Dockerfile ENV) | |
| if [ -n "$BACKUP_DIRECTORY" ]; then | |
| set -- "$@" "-Dbackup.directory=${BACKUP_DIRECTORY}" | |
| fi | |
| # Set scheduled users if provided | |
| if [ -n "$SCHEDULED_USERS" ]; then | |
| set -- "$@" "-Dbackup.scheduled.users=${SCHEDULED_USERS}" | |
| fi | |
| # Execute the application | |
| set -- "$@" -jar gh-backup.jar "$@" | |
| exec "$@" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package com.github.backup; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import jakarta.annotation.PostConstruct; | ||
| import java.io.IOException; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Service that runs scheduled backups at configured intervals. | ||
| * Enabled when backup.mode=daemon. | ||
| */ | ||
| @Service | ||
| @ConditionalOnProperty(name = "backup.mode", havingValue = "daemon") | ||
| public class ScheduledBackupService { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(ScheduledBackupService.class); | ||
| private static final int SEPARATOR_LENGTH = 80; | ||
|
|
||
| private final BackupService backupService; | ||
| private final List<String> scheduledUsers; | ||
| private final long backupIntervalMs; | ||
| private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||
|
|
||
| public ScheduledBackupService(BackupService backupService, | ||
| @Value("${backup.scheduled.users:}") String scheduledUsersConfig, | ||
| @Value("${backup.scheduled.interval.ms:86400000}") long backupIntervalMs) { | ||
| this.backupService = backupService; | ||
| this.backupIntervalMs = backupIntervalMs; | ||
| // Parse comma-separated list of users/orgs | ||
| this.scheduledUsers = scheduledUsersConfig.isBlank() | ||
| ? List.of() | ||
| : Arrays.stream(scheduledUsersConfig.split(",")) | ||
| .map(String::trim) | ||
| .filter(s -> !s.isEmpty()) | ||
| .toList(); | ||
| } | ||
|
|
||
| @PostConstruct | ||
| public void init() { | ||
| if (scheduledUsers.isEmpty()) { | ||
| log.warn("No users/organizations configured for scheduled backups."); | ||
| log.warn("Set the 'backup.scheduled.users' property with a comma-separated list."); | ||
| log.warn("Example: backup.scheduled.users=octocat,github"); | ||
| } else { | ||
| log.info("GitHub Backup Daemon Mode"); | ||
| log.info("========================"); | ||
| log.info("Scheduled backups enabled for: {}", String.join(", ", scheduledUsers)); | ||
| log.info("Backup interval: {} hours", backupIntervalMs / 3600000.0); | ||
| log.info("First backup will run immediately, then every {} hours.", backupIntervalMs / 3600000.0); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Runs backup for all configured users/organizations. | ||
| * Executes on startup and then at the configured interval after each completion. | ||
| * Uses fixedDelay to ensure the next backup starts only after the previous one completes. | ||
| */ | ||
| @Scheduled(fixedDelayString = "${backup.scheduled.interval.ms:86400000}", initialDelay = 0) | ||
| public void runScheduledBackup() { | ||
| if (scheduledUsers.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| String timestamp = LocalDateTime.now().format(dateTimeFormatter); | ||
| log.info(""); | ||
| log.info("{}", "=".repeat(SEPARATOR_LENGTH)); | ||
| log.info("Starting scheduled backup at {}", timestamp); | ||
| log.info("{}", "=".repeat(SEPARATOR_LENGTH)); | ||
|
|
||
| for (String userOrOrg : scheduledUsers) { | ||
| try { | ||
| backupService.backupUserRepositories(userOrOrg); | ||
| } catch (IOException e) { | ||
| log.error("Error backing up {}: {}", userOrOrg, e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| timestamp = LocalDateTime.now().format(dateTimeFormatter); | ||
| log.info(""); | ||
| log.info("{}", "=".repeat(SEPARATOR_LENGTH)); | ||
| log.info("Scheduled backup completed at {}", timestamp); | ||
| log.info("Next backup will run {} hours after this backup completes.", backupIntervalMs / 3600000.0); | ||
| log.info("{}", "=".repeat(SEPARATOR_LENGTH)); | ||
| log.info(""); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.github.backup; | ||
|
|
||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.Profile; | ||
| import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
|
||
| /** | ||
| * Configuration to enable Spring scheduling only in daemon mode. | ||
| * This prevents scheduling from being activated in CLI and web modes. | ||
| */ | ||
| @Configuration | ||
| @EnableScheduling | ||
| @Profile("daemon") | ||
| public class SchedulingConfiguration { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Daemon mode configuration | ||
| spring.main.web-application-type=none | ||
| backup.mode=daemon | ||
|
|
||
| # Comma-separated list of GitHub users/organizations to backup automatically | ||
| # Example: backup.scheduled.users=octocat,github,spring-projects | ||
| backup.scheduled.users= | ||
|
|
||
| # Backup interval in milliseconds (default: 86400000 = 24 hours) | ||
| # Example: 3600000 = 1 hour, 43200000 = 12 hours | ||
| backup.scheduled.interval.ms=86400000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are skipped during the Docker build with -DskipTests. While this speeds up the build, it means the Docker image could contain code that hasn't been tested. Consider running tests during the build to ensure code quality, or document why tests are skipped (e.g., if tests require external dependencies not available during build).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 721d02a. Added comments documenting that tests should be run in CI/CD pipeline before building the Docker image, and that tests are skipped to speed up image creation.