diff --git a/.github/actions/deploy/vercel/development/action.yml b/.github/actions/deploy/vercel/development/action.yml new file mode 100644 index 0000000..a6ae55a --- /dev/null +++ b/.github/actions/deploy/vercel/development/action.yml @@ -0,0 +1,101 @@ +name: 'Deploy to Vercel Development' +description: 'Deploys the application to Vercel Preview environment' + +inputs: + vercel-token: + description: 'Vercel authentication token' + required: true + vercel-org-id: + description: 'Vercel organization ID' + required: true + vercel-project-id: + description: 'Vercel project ID' + required: true + pr-number: + description: 'Pull request number' + required: true + +outputs: + deployment-url: + description: 'The URL of the deployed application' + value: ${{ steps.deploy.outputs.url }} + deployment-id: + description: 'The ID of the deployment' + value: ${{ steps.deploy.outputs.id }} + +runs: + using: 'composite' + steps: + - name: Pull Vercel Environment Information + shell: bash + run: | + echo "š Pulling Vercel environment information..." + echo " - Environment: Preview" + echo " - PR Number: ${{ inputs.pr-number }}" + vercel pull --yes --environment=preview --token=${{ inputs.vercel-token }} + echo "ā Successfully pulled Vercel environment information" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + + - name: Build Project + shell: bash + run: | + echo "šļø Starting build process for preview deployment..." + echo "š Exporting environment variables..." + # Export all GitHub secrets as environment variables + for secret in $(env | grep "GITHUB_.*=" | cut -d= -f1); do + echo " - Exporting $secret" + echo "$secret=${!secret}" >> $GITHUB_ENV + done + for secret in $(env | grep "INPUT_.*=" | cut -d= -f1); do + # Convert INPUT_VERCEL_TOKEN to VERCEL_TOKEN etc. + clean_name=$(echo "$secret" | sed 's/^INPUT_//' | tr '[:upper:]' '[:lower:]') + echo " - Converting and exporting $secret as $clean_name" + echo "$clean_name=${!secret}" >> $GITHUB_ENV + done + + echo "š Running build command..." + npm run build + + echo "š¦ Checking build output..." + if [ -d "dist" ]; then + echo " - dist directory exists" + echo " - Contents of dist directory:" + ls -la dist/ + else + echo "ā dist directory not found!" + exit 1 + fi + + echo "ā Preview build completed successfully" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + + - name: Deploy to Vercel + id: deploy + shell: bash + run: | + echo "š Starting Vercel preview deployment..." + echo "š Deploying dist directory..." + echo " - Current directory contents:" + ls -la dist/ + + echo "āļø Deploying to Vercel preview environment..." + DEPLOYMENT=$(vercel deploy --cwd dist --token=${{ inputs.vercel-token }} --yes) + DEPLOYMENT_URL="$DEPLOYMENT" + DEPLOYMENT_ID=$(echo "$DEPLOYMENT" | rev | cut -d'/' -f1 | rev) + + echo " - Deployment URL: $DEPLOYMENT_URL" + echo " - Deployment ID: $DEPLOYMENT_ID" + + echo "š¾ Saving deployment information..." + echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT + echo "id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT + + echo "ā Preview deployment completed successfully" + echo "š Preview URL: $DEPLOYMENT_URL" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} diff --git a/.github/actions/deploy/vercel/production/action.yml b/.github/actions/deploy/vercel/production/action.yml new file mode 100644 index 0000000..5108520 --- /dev/null +++ b/.github/actions/deploy/vercel/production/action.yml @@ -0,0 +1,94 @@ +name: 'Deploy to Vercel Production' +description: 'Deploys the application to Vercel Production environment' + +inputs: + vercel-token: + description: 'Vercel authentication token' + required: true + vercel-org-id: + description: 'Vercel organization ID' + required: true + vercel-project-id: + description: 'Vercel project ID' + required: true + sha: + description: 'Git commit SHA' + required: true + +outputs: + deployment-url: + description: 'The URL of the deployed application' + value: ${{ steps.deploy.outputs.url }} + +runs: + using: 'composite' + steps: + - name: Pull Vercel Environment Information + shell: bash + run: | + echo "š Pulling Vercel environment information..." + vercel pull --yes --environment=production --token=${{ inputs.vercel-token }} + echo "ā Successfully pulled Vercel environment information" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + + - name: Build Project + shell: bash + run: | + echo "šļø Starting build process..." + echo "š Exporting environment variables..." + # Export all GitHub secrets as environment variables + for secret in $(env | grep "GITHUB_.*=" | cut -d= -f1); do + echo " - Exporting $secret" + echo "$secret=${!secret}" >> $GITHUB_ENV + done + for secret in $(env | grep "INPUT_.*=" | cut -d= -f1); do + # Convert INPUT_VERCEL_TOKEN to VERCEL_TOKEN etc. + clean_name=$(echo "$secret" | sed 's/^INPUT_//' | tr '[:upper:]' '[:lower:]') + echo " - Converting and exporting $secret as $clean_name" + echo "$clean_name=${!secret}" >> $GITHUB_ENV + done + + echo "š Installing all dependencies..." + npm ci --include=dev + + echo "š Running build command..." + npm run build + + echo "š¦ Checking build output..." + if [ -d "dist" ]; then + echo " - dist directory exists" + echo " - Contents of dist directory:" + ls -la dist/ + else + echo "ā dist directory not found!" + exit 1 + fi + + echo "ā Build completed successfully" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + + - name: Deploy to Vercel + id: deploy + shell: bash + run: | + echo "š Starting Vercel deployment..." + echo "š Deploying dist directory..." + echo " - Current directory contents:" + ls -la dist/ + + echo "āļø Deploying to Vercel..." + DEPLOYMENT_URL=$(vercel deploy --cwd dist --prod --token=${{ inputs.vercel-token }} --yes) + echo " - Deployment URL: $DEPLOYMENT_URL" + + echo "š¾ Saving deployment URL..." + echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT + + echo "ā Deployment completed successfully" + echo "š Production URL: $DEPLOYMENT_URL" + env: + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} diff --git a/.github/actions/deployment-status/action.yml b/.github/actions/deployment-status/action.yml new file mode 100644 index 0000000..b628b27 --- /dev/null +++ b/.github/actions/deployment-status/action.yml @@ -0,0 +1,50 @@ +name: 'Update Deployment Status' +description: 'Updates the deployment status in GitHub' + +inputs: + environment: + description: 'The deployment environment (production/preview)' + required: true + deployment-url: + description: 'The URL of the deployed application' + required: true + sha: + description: 'Git commit SHA' + required: true + status: + description: 'Deployment status (success/failure)' + required: true + description: + description: 'Status description' + required: true + +runs: + using: 'composite' + steps: + - name: Update deployment status + shell: bash + run: | + echo "š Updating deployment status..." + echo " - Environment: ${{ inputs.environment }}" + echo " - Status: ${{ inputs.status }}" + echo " - URL: ${{ inputs.deployment-url }}" + echo " - Commit SHA: ${{ inputs.sha }}" + + echo "š Creating deployment record..." + DEPLOYMENT_ID=$(curl -s -X POST \ + -H "Authorization: token ${{ github.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "{\"ref\":\"${{ inputs.sha }}\",\"environment\":\"${{ inputs.environment }}\",\"auto_merge\":false}" \ + "https://api.github.com/repos/${{ github.repository }}/deployments" | jq -r '.id') + + echo " - Created deployment with ID: $DEPLOYMENT_ID" + + echo "š¤ Updating deployment status..." + STATUS_RESPONSE=$(curl -s -X POST \ + -H "Authorization: token ${{ github.token }}" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "{\"state\":\"${{ inputs.status }}\",\"environment_url\":\"${{ inputs.deployment-url }}\",\"description\":\"${{ inputs.description }}\"}" \ + "https://api.github.com/repos/${{ github.repository }}/deployments/$DEPLOYMENT_ID/statuses") + + echo "ā Deployment status updated successfully" + echo " - Description: ${{ inputs.description }}" diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml new file mode 100644 index 0000000..f14e2dd --- /dev/null +++ b/.github/actions/setup-environment/action.yml @@ -0,0 +1,34 @@ +name: 'Setup Environment' +description: 'Sets up Node.js and installs dependencies' + +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + shell: bash + run: | + echo "š§ Setting up environment..." + echo "š¦ Node.js version:" + node --version + + echo "š¦ NPM version:" + npm --version + + echo "š„ Installing dependencies..." + npm ci + + echo "š§ Installing Vercel CLI..." + npm install --global vercel@latest + echo " - Vercel CLI version:" + vercel --version + + echo "š Listing installed packages:" + npm list --depth=0 + + echo "ā Environment setup completed successfully" diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml new file mode 100644 index 0000000..cfe4301 --- /dev/null +++ b/.github/workflows/deploy-preview.yml @@ -0,0 +1,215 @@ +name: Deploy Preview + +on: + pull_request: + branches: [ master ] + types: [opened, synchronize, reopened] + +permissions: + contents: read + packages: read + pull-requests: write + deployments: write + id-token: write + +concurrency: + group: preview-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} + cancel-in-progress: true + +env: + NODE_ENV: development + HUSKY: 0 + +jobs: + security-check: + name: Security Check + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + pull-requests: read + outputs: + is-fork: ${{ steps.check.outputs.is-fork }} + is-authorized: ${{ steps.check.outputs.is-authorized }} + steps: + - name: Check PR source and permissions + id: check + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const isFork = pr.head.repo.full_name !== pr.base.repo.full_name; + + let isAuthorized = false; + try { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: pr.user.login + }); + isAuthorized = ['admin', 'write'].includes(permission.permission); + } catch (e) { + console.error('Error checking permissions:', e); + isAuthorized = false; + } + + core.setOutput('is-fork', isFork.toString()); + core.setOutput('is-authorized', isAuthorized.toString()); + + if (isFork && !isAuthorized) { + core.notice('ā ļø This PR is from a fork and requires approval from maintainers'); + } + + preview: + name: Deploy Preview + needs: security-check + runs-on: ubuntu-latest + if: | + github.event.workflow_run.conclusion != 'action_required' || + github.event.workflow_run.conclusion == 'approved' + environment: + name: preview + url: ${{ steps.deploy.outputs.deployment-url }} + permissions: + deployments: write + issues: write + pull-requests: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Additional security checks for forks + id: security_checks + if: needs.security-check.outputs.is-fork == 'true' + run: | + # Function to check file patterns + check_patterns() { + local file="$1" + local patterns=( + "crypto\." + "eval[\s]*\(" + "child_process" + "exec[A-Z][a-z]*\(" + "http[s]?\." + "net\." + "process\.env" + "require\(['\"]child_process" + "fs\." + "new\s+Function" + "__proto__" + "Function\(" + "require\(['\"]\.\." + "require\(['\"]~" + "process\.binding" + "v8\." + "vm\." + "\.constructor\." + "Object\.prototype" + "Object\.defineProperty" + "Object\.setPrototypeOf" + ) + + for pattern in "${patterns[@]}"; do + if grep -q "$pattern" "$file"; then + echo "ā ļø Suspicious pattern found in $file: $pattern" + return 1 + fi + done + return 0 + } + + exit_code=0 + while IFS= read -r file; do + if [ -f "$file" ]; then + if file "$file" | grep -q "binary"; then + echo "ā Binary file detected: $file" + exit_code=1 + fi + + if ! file "$file" | grep -q "binary"; then + if ! check_patterns "$file"; then + exit_code=1 + fi + fi + fi + done < <(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + + if [ $exit_code -eq 0 ]; then + echo "SECURITY_CHECK_RESULT=ā All security checks passed" >> $GITHUB_ENV + else + echo "SECURITY_CHECK_RESULT=ā ļø Security review required - See above for details" >> $GITHUB_ENV + exit 1 + fi + + - name: Setup environment + uses: ./.github/actions/setup-environment + + - name: Deploy to Vercel + id: deploy + timeout-minutes: 10 + uses: ./.github/actions/deploy/vercel/development + env: + # Pass all repository secrets to the action + REACT_APP_WS_PORT: ${{ secrets.REACT_APP_WS_PORT }} + REACT_APP_WS_URL: ${{ secrets.REACT_APP_WS_URL }} + REACT_OAUTH_URL: ${{ secrets.REACT_OAUTH_URL }} + REACT_CURRENT_ENVIRONMENT: preview + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + pr-number: ${{ github.event.pull_request.number }} + + - name: Add preview URL to pull request + uses: actions/github-script@v7 + with: + script: | + const isFork = '${{ needs.security-check.outputs.is-fork }}' === 'true'; + const isAuthorized = '${{ needs.security-check.outputs.is-authorized }}' === 'true'; + + let securityStatus = ''; + if (isFork) { + securityStatus = `\n\nš Security Status: + - PR is from a fork repository + - Author permission level: ${isAuthorized ? 'ā Authorized' : 'ā ļø Requires Approval'} + - Security checks: ${process.env.SECURITY_CHECK_RESULT || 'ā Passed'} + + Note: First-time contributors require maintainer approval for workflow runs.`; + } + + const deploymentUrl = '${{ steps.deploy.outputs.deployment-url }}'; + const comment = `āØ Preview deployment is ready! + + š Preview URL: ${deploymentUrl} + š Commit: ${context.sha.substring(0, 7)} + š Deployed at: ${new Date().toISOString()}${securityStatus}`; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + + - name: Update deployment status + uses: ./.github/actions/deployment-status + if: success() + with: + environment: 'preview' + deployment-url: ${{ steps.deploy.outputs.deployment-url }} + sha: ${{ github.event.pull_request.head.sha }} + status: 'success' + description: 'āØ Preview deployment completed' + + - name: Handle deployment failure + if: failure() + uses: ./.github/actions/deployment-status + with: + environment: 'preview' + deployment-url: ${{ steps.deploy.outputs.deployment-url }} + sha: ${{ github.event.pull_request.head.sha }} + status: 'failure' + description: 'ā Preview deployment failed' diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..44cc137 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,114 @@ +name: Deploy Production + +on: + push: + branches: + - master + +permissions: + contents: read + packages: read + pull-requests: write + deployments: write + id-token: write + +concurrency: + group: production + cancel-in-progress: false + +env: + NODE_ENV: production + HUSKY: 0 + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + +jobs: + validate: + name: Validate Deployment + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + secrets-valid: ${{ steps.check-secrets.outputs.valid }} + steps: + - name: Validate Required Secrets + id: check-secrets + run: | + echo "š Checking required secrets..." + missing_secrets=() + + if [ -z "$VERCEL_TOKEN" ]; then + missing_secrets+=("VERCEL_TOKEN") + fi + if [ -z "$VERCEL_ORG_ID" ]; then + missing_secrets+=("VERCEL_ORG_ID") + fi + if [ -z "$VERCEL_PROJECT_ID" ]; then + missing_secrets+=("VERCEL_PROJECT_ID") + fi + + if [ ${#missing_secrets[@]} -ne 0 ]; then + echo "ā Missing required secrets: ${missing_secrets[*]}" + echo "valid=false" >> $GITHUB_OUTPUT + exit 1 + fi + + echo "ā All required secrets are present" + echo "valid=true" >> $GITHUB_OUTPUT + + deploy: + name: Deploy Production + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: + name: production + url: ${{ steps.deploy.outputs.deployment-url }} + permissions: + deployments: write + statuses: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: ./.github/actions/setup-environment + + - name: Deploy to Vercel + id: deploy + timeout-minutes: 10 + uses: ./.github/actions/deploy/vercel/production + env: + REACT_APP_WS_PORT: ${{ secrets.REACT_APP_WS_PORT }} + REACT_APP_WS_URL: ${{ secrets.REACT_APP_WS_URL }} + REACT_OAUTH_URL: ${{ secrets.REACT_OAUTH_URL }} + REACT_CURRENT_ENVIRONMENT: production + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + sha: ${{ github.sha }} + + - name: Update deployment status + uses: ./.github/actions/deployment-status + if: success() + with: + environment: 'production' + deployment-url: ${{ steps.deploy.outputs.deployment-url }} + sha: ${{ github.sha }} + status: 'success' + description: 'āØ Production deployment completed' + + - name: Handle deployment failure + if: failure() + uses: ./.github/actions/deployment-status + with: + environment: 'production' + deployment-url: ${{ steps.deploy.outputs.deployment-url }} + sha: ${{ github.sha }} + status: 'failure' + description: 'ā Production deployment failed' diff --git a/index.html b/index.html index 554e4f2..0bba582 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@
-