diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f0e2f9c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules +npm-debug.log + +# Git +.git +.gitignore +.github + +# Build outputs +dist/index.js +dist/index.js.map + +# Development +.vscode +.idea + +# Testing +coverage +test + +# Documentation +README.md +LICENSE + +# Misc +*.md +.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..950ec85 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# API Configuration +# The base URL for the Purdue.io API +API_URL=https://api.purdue.io/odata diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml new file mode 100644 index 0000000..3df5a12 --- /dev/null +++ b/.github/workflows/docker-pr.yml @@ -0,0 +1,46 @@ +name: Docker PR Verification + +on: + pull_request: + branches: [ main ] + paths: + - 'Dockerfile' + - 'docker-compose.yml' + - 'nginx.conf' + - '.dockerignore' + - 'src/**' + - 'package*.json' + - 'webpack.config.js' + - 'tsconfig.json' + +jobs: + verify: + name: Verify Docker Build + runs-on: ubuntu-latest + permissions: + contents: read + + 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: . + file: ./Dockerfile + push: false + load: true + tags: purdueio-browser:pr-${{ github.event.pull_request.number }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Test image runs + run: | + docker run -d --name test-container -p 8080:80 purdueio-browser:pr-${{ github.event.pull_request.number }} + sleep 5 + curl -f http://localhost:8080 || exit 1 + docker stop test-container diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..123754e --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,58 @@ +name: Docker Publish + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Tag to build and push' + required: true + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 50343ae..73aa763 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,26 +9,28 @@ on: jobs: build: runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v4 - - name: Use Node.js 14.x - uses: actions/setup-node@v1 + - name: Use Node.js 18.x + uses: actions/setup-node@v4 with: - node-version: 14.x + node-version: 18.x + cache: 'npm' - - name: Restore Packages - run: npm install + - name: Install dependencies + run: npm ci - - name: Build and Publish + - name: Build run: npm run publish - + - name: Test run: npm run test - - name: Upload Artifact - uses: actions/upload-artifact@v2 + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: name: Browser path: dist diff --git a/.gitignore b/.gitignore index 02907f1..f91ab0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist/index.js dist/index.js.map -node_modules \ No newline at end of file +node_modules +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7f4e50f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Stage 1: Build the application +FROM node:18-alpine AS builder + +# Build argument for API URL +ARG API_URL=https://api.purdue.io/odata + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production=false + +# Copy source code +COPY . . + +# Build the application with API_URL environment variable +ENV API_URL=${API_URL} +RUN npm run publish + +# Stage 2: Serve with nginx +FROM nginx:alpine + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built application from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3788159 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + web: + build: + context: . + dockerfile: Dockerfile + args: + - API_URL=${API_URL:-https://api.purdue.io/odata} + ports: + - "8080:80" + restart: unless-stopped + container_name: purdueio-browser diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c7da6f7 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/ts/Application.ts b/src/ts/Application.ts index 2a5050c..1085dda 100644 --- a/src/ts/Application.ts +++ b/src/ts/Application.ts @@ -28,7 +28,8 @@ export class Application public static run(): Application { - let dataSource = new PurdueApiDataSource("https://api.purdue.io/odata"); + const apiUrl = process.env.API_URL || "https://api.purdue.io/odata"; + let dataSource = new PurdueApiDataSource(apiUrl); let returnVal = new Application(dataSource); returnVal.start(); return returnVal; diff --git a/webpack.config.js b/webpack.config.js index ec3eaed..c60c437 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require('path'); +const webpack = require('webpack'); var config = { entry: './src/index.ts', @@ -18,7 +19,12 @@ var config = { output: { filename: 'index.js', path: path.resolve(__dirname, 'dist'), - } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.API_URL': JSON.stringify(process.env.API_URL || 'https://api.purdue.io/odata') + }) + ] }; module.exports = (env, argv) => {