diff --git a/.github/workflows/web-container.yml b/.github/workflows/web-container.yml new file mode 100644 index 0000000..c910578 --- /dev/null +++ b/.github/workflows/web-container.yml @@ -0,0 +1,84 @@ +name: Web Deployment Container +on: + workflow_dispatch: {} + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - ready_for_review + push: + branches: + - main + paths: + - web/** +jobs: + determine-workflow: + runs-on: 'ubuntu-latest' + outputs: + workflow_type: ${{ steps.workflow.outputs.workflow_type }} + workflow_envs: ${{ steps.workflow.outputs.workflow_envs }} + release_type: ${{ steps.workflow.outputs.release_type }} + steps: + - name: Determine Workflow + id: workflow + shell: bash + run: | + event=${{ github.event_name }} + workflow_type='dev'; + workflow_envs='["dev"]' + if [[ $event == 'workflow_dispatch' && '${{ github.ref_name }}' == 'main' ]]; + then + echo "in if statement" + workflow_type='release'; + workflow_envs='["prod"]' + fi + + echo "workflow_type=$workflow_type" >> $GITHUB_OUTPUT + echo "workflow_envs=$workflow_envs" >> $GITHUB_OUTPUT + + echo "Running $workflow_type pipeline in environments: $workflow_envs" >> $GITHUB_STEP_SUMMARY + nonprod-deploy: + needs: determine-workflow + if: needs.determine-workflow.outputs.workflow_type != 'release' + strategy: + max-parallel: 1 + matrix: + env: ${{ fromJson(needs.determine-workflow.outputs.workflow_envs) }} + uses: ./.github/workflows/web-deploy-nonprod.yml + with: + environments: ${{ matrix.env }} + workflow_type: ${{ needs.determine-workflow.outputs.workflow_type }} + branch: ${{ github.head_ref || github.ref_name }} + secrets: + DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + TEST: ${{ secrets.TEST }} + COOLIFY_WEBHOOK: ${{ secrets.COOLIFY_WEBHOOK }} + COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} + permissions: + contents: read + packages: write + prod-deploy: + needs: determine-workflow + if: needs.determine-workflow.outputs.workflow_type == 'release' + strategy: + max-parallel: 1 + matrix: + env: ${{ fromJson(needs.determine-workflow.outputs.workflow_envs) }} + uses: ./.github/workflows/web-deploy-prod.yml + with: + environments: ${{ matrix.env }} + workflow_type: ${{ needs.determine-workflow.outputs.workflow_type }} + branch: ${{ github.head_ref || github.ref_name }} + secrets: + DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + TEST: ${{ secrets.TEST }} + COOLIFY_WEBHOOK: ${{ secrets.COOLIFY_WEBHOOK }} + COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} + permissions: + contents: read + packages: write diff --git a/.github/workflows/web-deploy-nonprod.yml b/.github/workflows/web-deploy-nonprod.yml new file mode 100644 index 0000000..ab168ef --- /dev/null +++ b/.github/workflows/web-deploy-nonprod.yml @@ -0,0 +1,89 @@ +name: Web Deployment Non-Production +on: + workflow_call: + inputs: + environments: + type: string + description: An optional list of environments to deploy to. + default: 'dev' + workflow_type: + type: string + description: An optional string for workflow types. + default: 'dev' + branch: + type: string + description: An optional string to define which branch to checkout. + default: 'main' + secrets: + DOCKERHUB_USER: {} + DOCKERHUB_TOKEN: {} + TEST: {} + COOLIFY_WEBHOOK: {} + COOLIFY_TOKEN: {} +jobs: + check-inputs: + runs-on: 'ubuntu-latest' + environment: ${{ inputs.environments }} + steps: + - name: Check secrets present + run: | + if [[ -z "${{ secrets.COOLIFY_WEBHOOK }}" ]]; then + echo "COOLIFY_WEBHOOK secret is empty or missing" + exit 1 + else + echo "COOLIFY_WEBHOOK secret is set" + fi + if [[ -z "${{ secrets.COOLIFY_TOKEN }}" ]]; then + echo "COOLIFY_TOKEN secret is empty or missing" + exit 1 + else + echo "COOLIFY_TOKEN secret is set" + fi + if [[ -z "${{ secrets.DOCKERHUB_USER }}" ]]; then + echo "DOCKERHUB_USER secret is empty or missing" + exit 1 + else + echo "DOCKERHUB_USER secret is set" + fi + if [[ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then + echo "DOCKERHUB_TOKEN secret is empty or missing" + exit 1 + else + echo "DOCKERHUB_TOKEN secret is set" + fi + build: + needs: check-inputs + if: needs.check-inputs.result == 'success' && inputs.workflow_type != 'release' + environment: ${{ inputs.environments }} + runs-on: 'ubuntu-latest' + permissions: + contents: read + packages: write + steps: + - name: Branch Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + - name: Login to Docker + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Docker Build Backend + run: docker build -f web/Dockerfile -t john4064/shiftsync:latest_web ./web --build-arg ENVIRONMENT=dev + - name: Docker Push Backend + run: docker push john4064/shiftsync:latest_web + deploy: + needs: build + if: needs.build.result == 'success' && inputs.workflow_type != 'release' + environment: ${{ inputs.environments }} + runs-on: 'ubuntu-latest' + permissions: + contents: read + packages: write + steps: + - name: Deploy to Coolify + run: | + curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' + diff --git a/.github/workflows/web-deploy-prod.yml b/.github/workflows/web-deploy-prod.yml new file mode 100644 index 0000000..09152e9 --- /dev/null +++ b/.github/workflows/web-deploy-prod.yml @@ -0,0 +1,89 @@ +name: Web Deployment Production +on: + workflow_call: + inputs: + environments: + type: string + description: An optional list of environments to deploy to. + default: 'prod' + workflow_type: + type: string + description: An optional string for workflow types. + default: 'prod' + branch: + type: string + description: An optional string to define which branch to checkout. + default: 'main' + secrets: + DOCKERHUB_USER: {} + DOCKERHUB_TOKEN: {} + TEST: {} + COOLIFY_WEBHOOK: {} + COOLIFY_TOKEN: {} +jobs: + check-inputs: + runs-on: 'ubuntu-latest' + environment: ${{ inputs.environments }} + steps: + - name: Check secrets present + run: | + if [[ -z "${{ secrets.COOLIFY_WEBHOOK }}" ]]; then + echo "COOLIFY_WEBHOOK secret is empty or missing" + exit 1 + else + echo "COOLIFY_WEBHOOK secret is set" + fi + if [[ -z "${{ secrets.COOLIFY_TOKEN }}" ]]; then + echo "COOLIFY_TOKEN secret is empty or missing" + exit 1 + else + echo "COOLIFY_TOKEN secret is set" + fi + if [[ -z "${{ secrets.DOCKERHUB_USER }}" ]]; then + echo "DOCKERHUB_USER secret is empty or missing" + exit 1 + else + echo "DOCKERHUB_USER secret is set" + fi + if [[ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then + echo "DOCKERHUB_TOKEN secret is empty or missing" + exit 1 + else + echo "DOCKERHUB_TOKEN secret is set" + fi + build: + needs: check-inputs + if: needs.check-inputs.result == 'success' && inputs.workflow_type == 'release' + environment: ${{ inputs.environments }} + runs-on: 'ubuntu-latest' + permissions: + contents: read + packages: write + steps: + - name: Branch Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + - name: Login to Docker + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Docker Build Backend + run: docker build -f web/Dockerfile -t john4064/shiftsync:prod_web ./web --build-arg ENVIRONMENT=prod + - name: Docker Push Backend + run: docker push john4064/shiftsync:prod_web + deploy: + needs: build + if: needs.build.result == 'success' && inputs.workflow_type == 'release' + environment: ${{ inputs.environments }} + runs-on: 'ubuntu-latest' + permissions: + contents: read + packages: write + steps: + - name: Deploy to Coolify + run: | + curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' + diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml deleted file mode 100644 index 613701e..0000000 --- a/.github/workflows/web-deploy.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Web Deployment -on: - workflow_dispatch: {} - pull_request: - branches: - - main - types: - - opened - - reopened - - synchronize - - ready_for_review - push: - branches: - - main - paths: - - web/** -jobs: - deploy: - environment: dev - runs-on: 'ubuntu-latest' - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to Docker - uses: docker/login-action@v3 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker Build Backend - run: docker build -f web/Dockerfile -t john4064/shiftsync:latest_web ./web --build-arg TEST=${{ secrets.TEST }} - - name: Docker Push Backend - run: docker push john4064/shiftsync:latest_web - - name: Deploy to Coolify - run: | - curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' - diff --git a/package-lock.json b/package-lock.json index deee5be..c3e5595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shiftsync-website", - "version": "1.0.1", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shiftsync-website", - "version": "1.0.1", + "version": "0.0.0", "dependencies": { "cors": "^2.8.5", "express": "^5.1.0" diff --git a/package.json b/package.json index 69ef0b7..99c9cbc 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "shiftsync-website", "private": true, - "version": "1.0.1", + "version": "0.0.0", "main": "index.js", "scripts": { "api": "npm run dev --prefix api", - "web": "npm run dev --prefix web", + "web": "npm run local --prefix web", "dev": "concurrently \"npm run api\" \"npm run web\"", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/web/Dockerfile b/web/Dockerfile index bb9805c..568eaa0 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -12,6 +12,9 @@ RUN npm ci COPY . ./ EXPOSE 5173 +EXPOSE 5171 +ARG ENVIRONMENT +ENV ENVIRONMENT ${ENVIRONMENT} -CMD ["npm", "run", "dev"] +CMD npm run ${ENVIRONMENT} diff --git a/web/docker-compose.yaml b/web/docker-compose.yaml index e5d5886..11d208e 100644 --- a/web/docker-compose.yaml +++ b/web/docker-compose.yaml @@ -1,6 +1,6 @@ services: shiftsync-web: - image: 'docker.io/john4064/shiftsync:latest_web' + image: 'docker.io/john4064/shiftsync:prod_web' environment: - 'TESTVAR=${COOLIFY_VAR}' volumes: diff --git a/web/index.html b/web/index.html index 05efba8..5ca2263 100644 --- a/web/index.html +++ b/web/index.html @@ -6,6 +6,8 @@ + + ShiftSync diff --git a/web/package-lock.json b/web/package-lock.json index 4fac8da..9512d68 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "shiftsync-website-web", - "version": "1.0.0", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shiftsync-website-web", - "version": "1.0.0", + "version": "1.0.5", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", @@ -947,9 +947,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -962,9 +962,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1045,19 +1045,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3257,9 +3270,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "dev": true, "funding": [ { diff --git a/web/package.json b/web/package.json index ba586dd..495e058 100644 --- a/web/package.json +++ b/web/package.json @@ -1,10 +1,12 @@ { "name": "shiftsync-website-web", "private": true, - "version": "1.0.0", + "version": "1.0.5", "type": "module", "scripts": { - "dev": "vite --host", + "local": "vite", + "dev": "vite --host --mode dev", + "prod": "vite --host --mode prod", "build": "vite build", "lint": "eslint .", "preview": "vite preview" diff --git a/web/public/hmr-runtime-inject.js b/web/public/hmr-runtime-inject.js new file mode 100644 index 0000000..6868556 --- /dev/null +++ b/web/public/hmr-runtime-inject.js @@ -0,0 +1,5 @@ +window.__vite_plugin_hmrOptions = { + protocol: location.protocol === 'https:' ? 'wss' : 'ws', + host: location.hostname, + port: location.port || (location.protocol === 'https:' ? 443 : 80), +}; \ No newline at end of file diff --git a/web/src/pages/Home/Home.jsx b/web/src/pages/Home/Home.jsx index e783380..4a75291 100644 --- a/web/src/pages/Home/Home.jsx +++ b/web/src/pages/Home/Home.jsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useLocalStore } from '@components'; +import pkg from '../../../package.json'; export const Home = () => { @@ -16,6 +17,7 @@ export const Home = () => {

Home Page

Go to Settings +

Version: {pkg.version}

); }; diff --git a/web/src/router/AppRouter.jsx b/web/src/router/AppRouter.jsx index 19d6710..5fa17a6 100644 --- a/web/src/router/AppRouter.jsx +++ b/web/src/router/AppRouter.jsx @@ -69,6 +69,16 @@ const AppRouter = () => { }; useEffect(() => { + const localVersion = localStorage.getItem("APP_VERSION"); + const currentVersion = window.APP_VERSION; + + if (localVersion && localVersion !== currentVersion) { + console.log("Version changed, forcing reload"); + localStorage.setItem("APP_VERSION", currentVersion); + window.location.reload(true); // force full page reload + } else { + localStorage.setItem("APP_VERSION", currentVersion); + } fetchAPI(); // await call for getting the count of employees and any other calls to db. diff --git a/web/vite.config.js b/web/vite.config.js index 6371a5f..c0e1bf1 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1,14 +1,44 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; +import pkg from './package.json'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + { + name: 'html-transform', + transformIndexHtml(html) { + return html + .replace(/__appVersion__/g, pkg.version) + } + } + ], + server:{ + host: true, + allowedHosts: true, + cors: true, + hmr: true + }, + optimizeDeps: { + include: ['react', 'react-dom'], + }, resolve: { + dedupe: ['react', 'react-dom'], alias: { '@src': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'components') }, }, + build: { + assetsDir: 'assets', + rollupOptions: { + output: { + entryFileNames: `assets/[name].[hash].js`, + chunkFileNames: `assets/[name].[hash].js`, + assetFileNames: `assets/[name].[hash].[ext]`, + }, + }, + } });