diff --git a/.github/workflows/deployy.yaml b/.github/workflows/deployy.yaml
new file mode 100644
index 0000000000..b860f02ed7
--- /dev/null
+++ b/.github/workflows/deployy.yaml
@@ -0,0 +1,60 @@
+name: Deployment Pipeline with Notifications
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '16'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: npm test
+
+ - name: Deploy to production
+ if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: |
+ echo "Deploying application..."
+ # Add your deployment commands here
+
+ notify:
+ needs: build_and_deploy
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Send Discord success notification
+ if: ${{ success() }}
+ uses: Ilshidur/action-discord@2.0.2
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message: |
+ :tada: **Deployment Successful!**
+ The latest version has been successfully deployed to production.
+ Commit: `${{ github.sha }}`
+ Branch: `${{ github.ref_name }}`
+
+ - name: Send Discord failure notification
+ if: ${{ failure() }}
+ uses: Ilshidur/action-discord@2.0.2
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message: |
+ :x: **Deployment Failed!**
+ Something went wrong during the deployment process.
+ Commit: `${{ github.sha }}`
+ Branch: `${{ github.ref_name }}`
+ Please review the logs for more details.
diff --git a/.github/workflows/fly.toml b/.github/workflows/fly.toml
new file mode 100644
index 0000000000..8d1c243698
--- /dev/null
+++ b/.github/workflows/fly.toml
@@ -0,0 +1,15 @@
+app = "part3-notes-backend-part4-8"
+primary_region = "arn"
+
+[http_service]
+auto_start_machines = true
+auto_stop_machines = true
+force_https = true
+internal_port = 3_001
+min_machines_running = 0
+processes = [ "app" ]
+
+[[vm]]
+cpu_kind = "shared"
+cpus = 1
+memory = "1gb"
diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml
new file mode 100644
index 0000000000..dccb01a3d8
--- /dev/null
+++ b/.github/workflows/fly.yml
@@ -0,0 +1,60 @@
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches: [main]
+
+jobs:
+ test_and_lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # Backend Tests
+ - name: Set up Node.js for backend
+ uses: actions/setup-node@v3
+ with:
+ node-version: '16'
+ - name: Install backend dependencies
+ run: |
+ cd backend
+ npm install
+ - name: Run backend tests
+ run: |
+ cd backend
+ npm test
+
+ # Frontend Tests
+ - name: Set up Node.js for frontend
+ uses: actions/setup-node@v3
+ with:
+ node-version: '16'
+ - name: Install frontend dependencies
+ run: |
+ cd frontend
+ npm install
+ - name: Run frontend tests
+ run: |
+ cd frontend
+ npm test
+
+ deploy:
+ needs: [test_and_lint]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # Deploy Backend
+ - name: Deploy Backend
+ run: |
+ echo "Deploying backend..."
+ # Add backend deployment script here
+
+ # Deploy Frontend
+ - name: Deploy Frontend
+ run: |
+ echo "Deploying frontend..."
+ # Add frontend deployment script here
diff --git a/.github/workflows/hello.yml b/.github/workflows/hello.yml
new file mode 100644
index 0000000000..915e731122
--- /dev/null
+++ b/.github/workflows/hello.yml
@@ -0,0 +1,20 @@
+name: Hello World
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ say_hello:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Print Hello World
+ run: echo "Hello World!"
+
+ - name: Print Date
+ run: date
+
+ - name: List Directory Contents
+ run: ls -l
+
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000000..53b15dde49
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,48 @@
+name: Deploy Application
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '16'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run tests
+ run: npm test
+
+ - name: Deploy to Production
+ run: npm run deploy
+ env:
+ MONGO_URI: ${{ secrets.MONGO_URI }}
+
+ - name: Notify Discord on Success
+ if: success() # Send notification only if the build succeeds
+ uses: Ilshidur/action-discord-webhook@v2
+ with:
+ webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ username: 'GitHub Actions'
+ content: 'The deployment was successful! 🎉'
+
+ - name: Notify Discord on Failure
+ if: failure() # Send notification only if the build fails
+ uses: Ilshidur/action-discord-webhook@v2
+ with:
+ webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ username: 'GitHub Actions'
+ content: 'The deployment failed! 🚨 Here is the commit that caused the issue: ${{ github.sha }}'
+
+
diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
new file mode 100644
index 0000000000..683802cebd
--- /dev/null
+++ b/.github/workflows/pipeline.yml
@@ -0,0 +1,47 @@
+name: CI Pipeline
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v3
+
+ - name: Install Dependencies
+ run: npm install
+
+ - name: Run Tests
+ run: npm test
+
+ - name: Test Success
+ if: success()
+ uses: rjstone/discord-webhook-notify@v1
+ with:
+ severity: info
+ details: Test Succeeded!
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
+
+ - name: Test Failure
+ if: failure()
+ uses: rjstone/discord-webhook-notify@v1
+ with:
+ severity: error
+ details: Test Failed!
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
+
+ - name: Test Cancelled
+ if: cancelled()
+ uses: rjstone/discord-webhook-notify@v1
+ with:
+ severity: warn
+ details: Test Cancelled!
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
diff --git a/.github/workflows/secrect.yml b/.github/workflows/secrect.yml
new file mode 100644
index 0000000000..63048fadf2
--- /dev/null
+++ b/.github/workflows/secrect.yml
@@ -0,0 +1,27 @@
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches: [main]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Dependencies
+ run: |
+ npm install
+
+ - name: Build Application
+ run: |
+ npm run build
+
+ - name: Deploy Application
+ run: |
+ echo "Deploying application..."
+ # Add your deployment commands here
diff --git a/README.md b/README.md
index d8424fe5c6..c22face70c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,10 @@
This repository is used for the CI/CD module of the Full stack open course
-Fork the repository to complete course exercises
+## Related Repositories
+
+- **Legacy Code**: The legacy code for the phonebook app and related exercises can be found here: [Fullstack Part 11 - Legacy Code](https://github.com/tuongroth/fullstack-part11)
+- **Pokedex App**: You can also check out the full-stack Pokedex app here: [Pokedex Repository](https://github.com/tuongroth/full-stack-open-pokedex)
## Commands
diff --git a/exercise11.1.md b/exercise11.1.md
new file mode 100644
index 0000000000..2b99555509
--- /dev/null
+++ b/exercise11.1.md
@@ -0,0 +1,9 @@
+Modern vs. Traditional CI Setup
+
+When setting up Continuous Integration (CI), the choice between modern cloud-based solutions and traditional self-hosted systems depends on your project’s needs, resources, and priorities.
+
+Modern CI solutions like GitHub Actions, CircleCI, and GitLab CI (Cloud) offer ease of use and scalability. They handle infrastructure, making them ideal for fast-paced teams focusing on development rather than system management. These tools work well with common linting, testing, and building tasks using tools like ESLint, Jest, or Webpack. However, they require reliable internet, may raise security concerns for sensitive data, and can become expensive for large-scale projects.
+
+Traditional CI systems like Jenkins or GitLab CI (Self-Hosted) give you full control over the environment. This makes them great for secure, highly customized setups where sensitive data or offline operation is critical. However, they require more effort for setup, maintenance, and scalability, which might strain small teams.
+
+In short, modern CI is best for flexibility and speed, while traditional CI suits teams prioritizing control and data security. Choose based on your project’s size, budget, and technical requirements.
diff --git a/test/PokemonPage.jest.spec.jsx b/test/PokemonPage.jest.spec.jsx
index 5a2b900545..210a1e9799 100644
--- a/test/PokemonPage.jest.spec.jsx
+++ b/test/PokemonPage.jest.spec.jsx
@@ -1,13 +1,13 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import axiosMock from 'axios'
-import { act } from 'react-dom/test-utils'
-import '@testing-library/jest-dom'
-import PokemonPage from '../src/PokemonPage'
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import axiosMock from 'axios';
+import { act } from 'react-dom/test-utils';
+import '@testing-library/jest-dom';
+import PokemonPage from '../src/PokemonPage';
-import { MemoryRouter } from 'react-router-dom'
+import { MemoryRouter } from 'react-router-dom';
-jest.mock('axios')
+jest.mock('axios');
const pokemonList = {
id: 133,
@@ -15,19 +15,19 @@ const pokemonList = {
{
ability: {
name: 'anticipation',
- url: 'https://pokeapi.co/api/v2/ability/107/'
+ url: 'https://pokeapi.co/api/v2/ability/107/',
},
is_hidden: true,
- slot: 3
+ slot: 3,
},
{
ability: {
name: 'adaptability',
- url: 'https://pokeapi.co/api/v2/ability/91/'
+ url: 'https://pokeapi.co/api/v2/ability/91/',
},
is_hidden: false,
- slot: 2
- }
+ slot: 2,
+ },
],
name: 'eevee',
stats: [
@@ -36,99 +36,106 @@ const pokemonList = {
effort: 0,
stat: {
name: 'attack',
- url: 'https://pokeapi.co/api/v2/stat/2/'
- }
+ url: 'https://pokeapi.co/api/v2/stat/2/',
+ },
},
{
base_stat: 55,
effort: 0,
stat: {
name: 'hp',
- url: 'https://pokeapi.co/api/v2/stat/1/'
- }
- }
+ url: 'https://pokeapi.co/api/v2/stat/1/',
+ },
+ },
],
types: [
{
slot: 1,
type: {
name: 'normal',
- url: 'https://pokeapi.co/api/v2/type/1/'
- }
- }
+ url: 'https://pokeapi.co/api/v2/type/1/',
+ },
+ },
],
- sprites: { front_default: 'URL' }
-}
+ sprites: { front_default: 'URL' },
+};
const previous = {
url: 'https://pokeapi.co/api/v2/pokemon/132/',
name: 'ditto',
- id: 132
-}
+ id: 132,
+};
const next = {
url: 'https://pokeapi.co/api/v2/pokemon/134/',
name: 'vaporeon',
- id: 134
-}
+ id: 134,
+};
describe('', () => {
+ beforeEach(() => {
+ jest.clearAllMocks(); // Ensure no previous mocks interfere.
+ });
+
it('should render abilities', async () => {
- axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
+ axiosMock.get.mockResolvedValueOnce({ data: pokemonList });
await act(async () => {
render(
,
- )
- })
+ );
+ });
- expect(screen.getByText('adaptability')).toBeVisible()
- expect(screen.getByText('anticipation')).toBeVisible()
- })
+ expect(screen.getByText('adaptability')).toBeVisible();
+ expect(screen.getByText('anticipation')).toBeVisible();
+ });
it('should render stats', async () => {
- axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
+ axiosMock.get.mockResolvedValueOnce({ data: pokemonList });
await act(async () => {
render(
,
- )
- })
+ );
+ });
- expect(screen.getByTestId('stats')).toHaveTextContent('hp55attack55')
- })
+ expect(screen.getByTestId('stats')).toHaveTextContent('hp55attack55');
+ });
- it('should render previous and next urls if they exist', async () => {
- axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
+ it('should render previous and next URLs if they exist', async () => {
+ axiosMock.get.mockResolvedValueOnce({ data: pokemonList });
await act(async () => {
render(
-
+
,
- )
- })
+ );
+ });
+
+ const previousLink = screen.getByText('Previous');
+ const nextLink = screen.getByText('Next');
- expect(screen.getByText('Previous')).toHaveAttribute('href', '/pokemon/ditto')
- expect(screen.getByText('Next')).toHaveAttribute('href', '/pokemon/vaporeon')
- })
+ expect(previousLink).toHaveAttribute('href', '/pokemon/ditto');
+ expect(nextLink).toHaveAttribute('href', '/pokemon/vaporeon');
+ });
- it('should not render previous and next urls if none exist', async () => {
- axiosMock.get.mockResolvedValueOnce({ data: pokemonList })
+ it('should not render previous and next URLs if none exist', async () => {
+ axiosMock.get.mockResolvedValueOnce({ data: pokemonList });
await act(async () => {
render(
,
- )
- })
+ );
+ });
- expect(screen.queryByText('Previous')).toBeNull()
- expect(screen.queryByText('Next')).toBeNull()
- })
-})
+ expect(screen.queryByText('Previous')).toBeNull();
+ expect(screen.queryByText('Next')).toBeNull();
+ });
+});