From a6abddaf55f8640122f6dd0d59d7b903872bac50 Mon Sep 17 00:00:00 2001 From: DarkShadow Date: Fri, 2 Jan 2026 17:45:52 +0200 Subject: [PATCH 1/7] Refactor CI workflow and add static analysis and Docker scan --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e999dd..e9731c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,12 +83,12 @@ jobs: run: snyk code test --severity-threshold=high --fail-on=all docker-scan: - name: Test Docker Image with Trivy - runs-on: ubuntu-latest - needs: [build] - permissions: - security-events: write - steps: + name: Test Docker Image with Trivy + runs-on: ubuntu-latest + needs: [build] + permissions: + security-events: write + steps: - name: Checkout the code uses: actions/checkout@v4 From cec79720c5ae186e737c411e87ed6198653f7142 Mon Sep 17 00:00:00 2001 From: DarkShadow Date: Fri, 2 Jan 2026 17:46:24 +0200 Subject: [PATCH 2/7] Add CD pipeline --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9731c6..1e999dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,12 +83,12 @@ jobs: run: snyk code test --severity-threshold=high --fail-on=all docker-scan: - name: Test Docker Image with Trivy - runs-on: ubuntu-latest - needs: [build] - permissions: - security-events: write - steps: + name: Test Docker Image with Trivy + runs-on: ubuntu-latest + needs: [build] + permissions: + security-events: write + steps: - name: Checkout the code uses: actions/checkout@v4 From d22c8141a820bda8b1e88fb11f663aef0ff1856a Mon Sep 17 00:00:00 2001 From: Ivan Panayotov Date: Mon, 5 Jan 2026 22:16:39 +0200 Subject: [PATCH 3/7] Update CD Pipeline --- Dockerfile | 2 +- k8s/service.yml | 2 +- .../client/SpotifyClient.java | 9 +++++---- .../server/SpotifyServer.java | 19 +++++++++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf827d1..25bd84d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ COPY --from=builder /app/target/*.jar app.jar EXPOSE 1337 -ENTRYPOINT ["java", "-jar", "app.jar", "--SERVER_HOST=0.0.0.0", "--SERVER_PORT=1337"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar", "0.0.0.0", "1337"] \ No newline at end of file diff --git a/k8s/service.yml b/k8s/service.yml index 77a592a..a226c40 100644 --- a/k8s/service.yml +++ b/k8s/service.yml @@ -11,5 +11,5 @@ spec: - protocol: TCP port: 1337 targetPort: 1337 - nodePort: 30007 + nodePort: 30008 diff --git a/src/main/java/com/spotifyremastered/client/SpotifyClient.java b/src/main/java/com/spotifyremastered/client/SpotifyClient.java index 3cd7b51..9d18be9 100644 --- a/src/main/java/com/spotifyremastered/client/SpotifyClient.java +++ b/src/main/java/com/spotifyremastered/client/SpotifyClient.java @@ -44,17 +44,18 @@ public class SpotifyClient { public static void main(String[] args) { SpotifyClient client = new SpotifyClient(); - final int maxArg = 3; + final int maxArg = 2; if (args.length >= maxArg) { - client.startClient(args[1], Integer.parseInt(args[2])); + client.startClient(args[0], Integer.parseInt(args[1])); + } else { + client.startClient(SERVER_HOST, SERVER_PORT); } - - client.startClient(SERVER_HOST, SERVER_PORT); } private void startClient(String ipAddress, int port) { try (SocketChannel socketChannel = SocketChannel.open(); Scanner scanner = new Scanner(System.in)) { + System.out.println("Attempting to connect to: " + ipAddress + ":" + port + "."); socketChannel.connect(new InetSocketAddress(ipAddress, port)); System.out.println("Connected to the server. Type 'help' for available commands."); diff --git a/src/main/java/com/spotifyremastered/server/SpotifyServer.java b/src/main/java/com/spotifyremastered/server/SpotifyServer.java index d35c378..6e15504 100644 --- a/src/main/java/com/spotifyremastered/server/SpotifyServer.java +++ b/src/main/java/com/spotifyremastered/server/SpotifyServer.java @@ -5,6 +5,7 @@ import com.spotifyremastered.server.command.executor.CommandExecutor; import com.spotifyremastered.server.command.factory.CommandFactory; import com.spotifyremastered.server.user.User; + import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; @@ -20,8 +21,8 @@ public class SpotifyServer { - private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 1337; + private static final String SERVER_HOST = System.getenv().getOrDefault("SERVER_HOST", "0.0.0.0"); + private static final int SERVER_PORT = Integer.parseInt(System.getenv().getOrDefault("SERVER_PORT", "1337")); private static final int BUFFER_SIZE = 1024; private static boolean online = true; @@ -36,13 +37,19 @@ public SpotifyServer() { public static void main(String[] args) { SpotifyServer server = new SpotifyServer(); - server.startServer(); + + final int maxArg = 2; + if (args.length >= maxArg) { + server.startServer(args[0], Integer.parseInt(args[1])); + } else { + server.startServer(SERVER_HOST, SERVER_PORT); + } } - public void startServer() { + public void startServer(String ipAddress, int port) { try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { - System.out.println("Server listening on port " + SERVER_PORT); - serverSocketChannel.bind(new InetSocketAddress(SERVER_HOST, SERVER_PORT)); + System.out.println("Server listening on address: " + ipAddress + ":" + port + "."); + serverSocketChannel.bind(new InetSocketAddress(ipAddress, port)); serverSocketChannel.configureBlocking(false); Selector selector = Selector.open(); From 08508ea4c20efede6a234c4bd025383797fdd7e8 Mon Sep 17 00:00:00 2001 From: Ivan Panayotov Date: Tue, 6 Jan 2026 01:44:29 +0200 Subject: [PATCH 4/7] Refactor synchronized with concurrent collections (#4) --- .../server/SpotifyManager.java | 42 +++++++++---------- .../server/SpotifyServer.java | 4 +- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/spotifyremastered/server/SpotifyManager.java b/src/main/java/com/spotifyremastered/server/SpotifyManager.java index dce6099..c759b25 100644 --- a/src/main/java/com/spotifyremastered/server/SpotifyManager.java +++ b/src/main/java/com/spotifyremastered/server/SpotifyManager.java @@ -2,15 +2,16 @@ import com.spotifyremastered.server.music.MusicManager; import com.spotifyremastered.server.user.UserManager; + import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class SpotifyManager { private final UserManager userManager; private final MusicManager musicManager; - private final Map portsManager; - private final Object lock = new Object(); + private final Map portsManager = new ConcurrentHashMap<>(); private static final short FIRST_AVAILABLE_PORT = 1338; private static final short MAX_PORTS = 1000; @@ -18,13 +19,11 @@ public class SpotifyManager { public SpotifyManager() { this.userManager = new UserManager(new HashMap<>(), new HashMap<>(), UserManager.USER_DATA_FILE_PATH); this.musicManager = new MusicManager(); - this.portsManager = new HashMap<>(); } public SpotifyManager(UserManager userManager, MusicManager musicManager) { this.userManager = userManager; this.musicManager = musicManager; - this.portsManager = new HashMap<>(); } public UserManager getUserManager() { @@ -36,39 +35,38 @@ public MusicManager getMusicManager() { } public Map getPortsManager() { - synchronized (lock) { - return new HashMap<>(portsManager); - } + return Map.copyOf(portsManager); } public int getNewPort(Client client) { - synchronized (lock) { - int maxPort = FIRST_AVAILABLE_PORT + MAX_PORTS; - for (int i = FIRST_AVAILABLE_PORT; i < maxPort; i++) { - if (isPortAvailable(i)) { - portsManager.put(client, i); + Integer existingPort = portsManager.get(client); + if (existingPort != null) { + return existingPort; + } + + int maxPort = FIRST_AVAILABLE_PORT + MAX_PORTS; + for (int i = FIRST_AVAILABLE_PORT; i < maxPort; i++) { + if (isPortAvailable(i)) { + Integer previous = portsManager.putIfAbsent(client, i); + if (previous == null) { return i; + } else { + return previous; } } - throw new RuntimeException("No available ports"); } + throw new RuntimeException("No available ports"); } public void removePort(Client client) { - synchronized (lock) { - portsManager.remove(client); - } + portsManager.remove(client); } public boolean isPortAvailable(int port) { - synchronized (lock) { - return !portsManager.containsValue(port); - } + return !portsManager.containsValue(port); } public boolean isPortAvailable(Client client) { - synchronized (lock) { - return portsManager.containsValue(portsManager.get(client)); - } + return portsManager.containsKey(client); } } diff --git a/src/main/java/com/spotifyremastered/server/SpotifyServer.java b/src/main/java/com/spotifyremastered/server/SpotifyServer.java index 6e15504..5d86391 100644 --- a/src/main/java/com/spotifyremastered/server/SpotifyServer.java +++ b/src/main/java/com/spotifyremastered/server/SpotifyServer.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class SpotifyServer { @@ -28,10 +29,9 @@ public class SpotifyServer { private static boolean online = true; private final SpotifyManager spotifyManager; - private final Map clients; + private final Map clients = new ConcurrentHashMap<>(); public SpotifyServer() { - this.clients = new HashMap<>(); this.spotifyManager = new SpotifyManager(); } From 5d6387cad926f63f246f0420ba2fdd5e579ff34b Mon Sep 17 00:00:00 2001 From: Ivan Panayotov Date: Tue, 6 Jan 2026 11:56:25 +0200 Subject: [PATCH 5/7] Feature/optimize pipelines (#5) --- .github/workflows/cd.yml | 2 ++ .github/workflows/ci.yml | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9fbcd5c..cc8762f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -6,6 +6,8 @@ on: - completed branches: - dev + + workflow_dispatch: jobs: build-and-push: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e999dd..2b0e2a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,17 @@ name: CI Pipeline on: push: branches: [ "main", "dev" ] + paths-ignore: + - '**/*.md' + - '.gitignore' + - '**/.gitignore' pull_request: branches: [ "main", "dev" ] - + paths-ignore: + - '**/*.md' + - '.gitignore' + - '**/.gitignore' + workflow_dispatch: jobs: build: name: Build and Test From 5f9715a7e2c6284846d03a292ee9dc9ae180a904 Mon Sep 17 00:00:00 2001 From: Ivan Panayotov Date: Tue, 6 Jan 2026 12:10:42 +0200 Subject: [PATCH 6/7] Feature/update readme (#6) --- README-devops.md | 37 +++++++++++++++++++++++++++++++++++++ README.md | 3 +++ 2 files changed, 40 insertions(+) create mode 100644 README-devops.md diff --git a/README-devops.md b/README-devops.md new file mode 100644 index 0000000..8e2792d --- /dev/null +++ b/README-devops.md @@ -0,0 +1,37 @@ +## DevOps & CI/CD Workflow + +This project implements a robust, modern DevOps pipeline that automates software delivery following several key best practices: + +### Branching Strategy + +All development work is done on feature branches, which are then merged into the `dev` branch after review and testing. + +Once features are considered stable and ready for release, changes from `dev` are merged into `main`, which is the protected branch and represents production-ready code. + +### CI/CD Pipeline Overview + +The delivery process is fully automated using GitHub Actions. +Key pipeline steps include: + +1. **Open Issue & Feature Branch Creation:** + - Development starts by opening an issue and creating a feature branch from `dev`. +2. **Continuous Integration on PRs and Pushes to main/dev:** + - Triggered by push or pull request on `main` or `dev` branches. + - **Build & Unit Test:** Automatically compiles code and runs Java unit tests (JUnit via Maven). + - **Linter & Style Check:** Runs Checkstyle and SpotBugs for static code analysis and style enforcement. + - **SAST (Static Application Security Testing):** Uses Snyk for security scanning with enforcement (pipeline fails on severe issues). + - **Build Docker Image & Scan:** + - Builds Docker image. + - Runs Trivy for Docker vulnerability scanning. +3. **Continuous Delivery (CD) to Kubernetes (K8s):** + - Triggered after a successful CI run on `dev`. + - **Docker Push:** Pushes the Docker image to Docker Hub. + - **Kubernetes Deployment:** + - Uses Minikube for testing. + - Deploys the built image using Kubernetes manifests from the `/k8s` directory. + - Monitors rollout and enables rollback on deployment failure. + +### Future Improvements +- Add a relational database (e.g., PostgreSQL) for user profiles, playlists, or playback history. +- Store uploaded songs in AWS S3 for scalable, cloud-based storage. +- Deploy a test environment on AWS EC2 for integration and end-to-end test automation. \ No newline at end of file diff --git a/README.md b/README.md index 42f2618..8027489 100644 --- a/README.md +++ b/README.md @@ -46,5 +46,8 @@ The server will send the appropriate response if a command was successful or not - Music streaming occurs on a separate thread, allowing you to contiunue sending new commands. - If something crashes or goes wrong, a logger will output a `.txt` file with the error log and additional details. +## DevOps & CI/CD +For a detailed description of the automated delivery pipeline, branching strategy, and DevOps practices implemented in this project, see [DevOps & CI/CD Workflow](README-devops.md). + # Credits Special thanks to Stoyan Velev and his team for their exceptional course! Be sure to check it out! From 1863f0a3f6dbd8d5a8b01ac84ac28be6e8732e49 Mon Sep 17 00:00:00 2001 From: Ivan Panayotov Date: Wed, 7 Jan 2026 00:14:17 +0200 Subject: [PATCH 7/7] Update CD pipeline for main branch and self-hosted runner --- .github/workflows/cd.yml | 73 +++++++++++++++++++++++++--------------- README-devops.md | 11 +++--- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index cc8762f..02b0e5e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,60 +1,79 @@ name: CD Pipeline on: - workflow_run: - workflows: ["CI Pipeline"] - types: - - completed - branches: - - dev - - workflow_dispatch: -jobs: + push: + branches: [ "main" ] + paths-ignore: + - '**/*.md' + - '.gitignore' + - '**/.gitignore' + workflow_dispatch: +jobs: build-and-push: name: Build and Push the Docker Container - if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest + runs-on: self-hosted + steps: - name: Checkout uses: actions/checkout@v4 + - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Build and push uses: docker/build-push-action@v5 with: context: ./ file: Dockerfile push: true - tags: ${{ secrets.DOCKER_USERNAME }}/spotifyremastered:latest + tags: | + ${{ secrets.DOCKER_USERNAME }}/spotifyremastered:latest + ${{ secrets.DOCKER_USERNAME }}/spotifyremastered:${{ github.sha }} k8s-deployment: name: Deploy the app - runs-on: ubuntu-latest + runs-on: self-hosted needs: [build-and-push] + steps: - - name: Setup Minikube - uses: medyagh/setup-minikube@master - name: Checkout uses: actions/checkout@v4 - - name: Minikube Docker Pull + + - name: Verify Kubernetes Cluster Access run: | - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker pull ${{ secrets.DOCKER_USERNAME }}/spotifyremastered:latest - minikube image load ${{ secrets.DOCKER_USERNAME }}/spotifyremastered:latest + echo "Checking Kubernetes cluster connection..." + kubectl cluster-info + kubectl get nodes + - name: Deploy to Kubernetes run: | - kubectl apply -f k8s/ - kubectl rollout status deployment/spotify-remastered --timeout=5m - - name: Rollback Deployment (if failed) + kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/deployment.yaml + kubectl apply -f k8s/service.yaml + + echo "Forcing rollout restart to pull latest image..." + kubectl rollout restart deployment/spotify-remastered -n spotify-app + + - name: Wait for Rollout to Complete + run: | + echo "Waiting for deployment to be ready..." + kubectl rollout status deployment/spotify-remastered -n spotify-app --timeout=5m + + - name: Verify Deployment + run: | + echo "Deployment successful! Current status:" + kubectl get pods -n spotify-app + kubectl get services -n spotify-app + + - name: Rollback on Failure if: failure() run: | - kubectl rollout undo deployment/spotify-remastered - - name: Check Kubernetes Status - run: kubectl get all - - name: Stop Minikube - run: minikube stop \ No newline at end of file + echo "Deployment failed! Rolling back to previous version..." + kubectl rollout undo deployment/spotify-remastered -n spotify-app + kubectl rollout status deployment/spotify-remastered -n spotify-app --timeout=3m \ No newline at end of file diff --git a/README-devops.md b/README-devops.md index 8e2792d..7317ce5 100644 --- a/README-devops.md +++ b/README-devops.md @@ -24,12 +24,11 @@ Key pipeline steps include: - Builds Docker image. - Runs Trivy for Docker vulnerability scanning. 3. **Continuous Delivery (CD) to Kubernetes (K8s):** - - Triggered after a successful CI run on `dev`. - - **Docker Push:** Pushes the Docker image to Docker Hub. - - **Kubernetes Deployment:** - - Uses Minikube for testing. - - Deploys the built image using Kubernetes manifests from the `/k8s` directory. - - Monitors rollout and enables rollback on deployment failure. + - Triggered on push to `main` or manually via workflow dispatch. + - **Two-stage pipeline:** + - **Build & Push:** Builds and pushes Docker image to Docker Hub with `latest` and commit SHA tags. + - **Deploy:** Applies Kubernetes manifests (namespace, deployment, service) to local Minikube cluster via self-hosted runner. + - Features rolling restart, deployment monitoring, and automatic rollback on failure. ### Future Improvements - Add a relational database (e.g., PostgreSQL) for user profiles, playlists, or playback history.