diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e6aea70 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @muhammadhammad2005 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9f9dae8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Summary + +- describe what changed +- explain why this change is needed + +## Checklist + +- [ ] I created a branch for this work and did not push directly to `main` +- [ ] I ran local checks if needed +- [ ] I updated docs if behavior changed +- [ ] This change is ready for review diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 70b7685..1900942 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -1,16 +1,24 @@ -name: Production CI/CD +name: Continuous Integration on: push: branches: - main + - develop + - feature/** + - fix/** + - hotfix/** + - chore/** + - refactor/** + tags-ignore: + - "v*.*.*" pull_request: branches: - main workflow_dispatch: concurrency: - group: production-cicd-${{ github.ref }} + group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: @@ -18,8 +26,6 @@ permissions: env: NODE_VERSION: "20" - IMAGE_NAME: resume-matcher - REGISTRY_IMAGE: ghcr.io/${{ github.repository }} jobs: quality: @@ -30,6 +36,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 @@ -64,6 +72,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 @@ -74,6 +84,11 @@ jobs: - name: Install dependencies run: npm ci + - name: Run gitleaks secret scan + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run npm audit run: npm audit --audit-level=high @@ -90,6 +105,7 @@ jobs: run: | echo "# Security Checks" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- gitleaks secret scan executed" >> "$GITHUB_STEP_SUMMARY" echo "- npm audit executed at high severity threshold" >> "$GITHUB_STEP_SUMMARY" echo "- Snyk dependency scan executed" >> "$GITHUB_STEP_SUMMARY" @@ -128,132 +144,6 @@ jobs: if: always() run: docker rm -f resume-matcher-ci - docker-publish: - name: Publish Container Images - runs-on: ubuntu-latest - needs: - - docker-smoke - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - timeout-minutes: 20 - 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 GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Generate image metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.REGISTRY_IMAGE }} - ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,prefix=sha- - type=ref,event=branch - - - name: Build and push multi-platform images - uses: docker/build-push-action@v6 - with: - context: . - target: production - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Container publish summary - if: always() - run: | - echo "# Container Publish" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- GHCR image: \`${{ env.REGISTRY_IMAGE }}\`" >> "$GITHUB_STEP_SUMMARY" - echo "- Docker Hub image: \`${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}\`" >> "$GITHUB_STEP_SUMMARY" - echo "- Tags: ${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY" - - deploy-vercel: - name: Deploy to Vercel Production - runs-on: ubuntu-latest - needs: - - quality - - security - - docker-publish - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - timeout-minutes: 15 - environment: - name: production - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Install Vercel CLI - run: npm install --global vercel@latest - - - name: Pull Vercel environment - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - - - name: Build Vercel output - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - - - name: Deploy prebuilt output - id: deploy - run: | - deployment_url="$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})" - echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT" - echo "Deployment URL: $deployment_url" - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - - - name: Verify production deployment - run: | - deployment_url="${{ steps.deploy.outputs.deployment_url }}" - vercel curl --deployment "$deployment_url" / --token=${{ secrets.VERCEL_TOKEN }} > /dev/null - vercel curl --deployment "$deployment_url" /health --token=${{ secrets.VERCEL_TOKEN }} > /dev/null - - - name: Deployment summary - if: always() - run: | - echo "# Vercel Deployment" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- Production URL: ${{ steps.deploy.outputs.deployment_url }}" >> "$GITHUB_STEP_SUMMARY" - echo "- Deployment strategy: CLI prebuilt deploy after successful pipeline gates" >> "$GITHUB_STEP_SUMMARY" - notify: name: Notification Stage runs-on: ubuntu-latest @@ -264,8 +154,6 @@ jobs: - quality - security - docker-smoke - - docker-publish - - deploy-vercel if: always() steps: @@ -274,14 +162,12 @@ jobs: run: | if [[ "${{ needs.quality.result }}" == "success" && \ "${{ needs.security.result }}" == "success" && \ - "${{ needs.docker-smoke.result }}" == "success" && \ - ( "${{ needs.docker-publish.result }}" == "success" || "${{ needs.docker-publish.result }}" == "skipped" ) && \ - ( "${{ needs.deploy-vercel.result }}" == "success" || "${{ needs.deploy-vercel.result }}" == "skipped" ) ]]; then + "${{ needs.docker-smoke.result }}" == "success" ]]; then echo "state=success" >> "$GITHUB_OUTPUT" - echo "message=Pipeline completed successfully." >> "$GITHUB_OUTPUT" + echo "message=CI pipeline completed successfully. No production deployment was triggered." >> "$GITHUB_OUTPUT" else echo "state=failure" >> "$GITHUB_OUTPUT" - echo "message=Pipeline failed. Review the workflow logs before deploying again." >> "$GITHUB_OUTPUT" + echo "message=CI pipeline failed. Review the workflow logs before merging." >> "$GITHUB_OUTPUT" fi - name: Send pipeline email notification @@ -292,7 +178,7 @@ jobs: server_port: 587 username: ${{ env.SMTP_USER }} password: ${{ env.SMTP_PASS }} - subject: "[${{ steps.status.outputs.state }}] ${{ github.repository }} CI/CD" + subject: "[${{ steps.status.outputs.state }}] ${{ github.repository }} CI" to: ${{ env.SMTP_USER }} from: GitHub Actions <${{ env.SMTP_USER }}> body: | @@ -303,8 +189,6 @@ jobs: Quality Gate: ${{ needs.quality.result }} Security Checks: ${{ needs.security.result }} Docker Smoke Test: ${{ needs.docker-smoke.result }} - Docker Publish: ${{ needs.docker-publish.result }} - Vercel Deploy: ${{ needs.deploy-vercel.result }} ${{ steps.status.outputs.message }} Run URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -317,7 +201,5 @@ jobs: echo "- Quality Gate: ${{ needs.quality.result }}" >> "$GITHUB_STEP_SUMMARY" echo "- Security Checks: ${{ needs.security.result }}" >> "$GITHUB_STEP_SUMMARY" echo "- Docker Smoke Test: ${{ needs.docker-smoke.result }}" >> "$GITHUB_STEP_SUMMARY" - echo "- Docker Publish: ${{ needs.docker-publish.result }}" >> "$GITHUB_STEP_SUMMARY" - echo "- Vercel Deploy: ${{ needs.deploy-vercel.result }}" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "${{ steps.status.outputs.message }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d0e28ee --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,291 @@ +name: Production Release + +on: + push: + tags: + - "v*.*.*" + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + packages: write + +env: + NODE_VERSION: "20" + IMAGE_NAME: resume-matcher + REGISTRY_IMAGE: ghcr.io/${{ github.repository }} + +jobs: + release-quality: + name: Release Quality Gate + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout release code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate release build + run: npm run ci:validate + + release-security: + name: Release Security Checks + runs-on: ubuntu-latest + needs: release-quality + timeout-minutes: 15 + + steps: + - name: Checkout release code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run gitleaks secret scan + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run npm audit + run: npm audit --audit-level=high + + - name: Run Snyk dependency scan + uses: snyk/actions/node@v1.0.0 + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + args: --severity-threshold=high --all-projects + + release-docker-smoke: + name: Release Docker Smoke Test + runs-on: ubuntu-latest + needs: + - release-quality + - release-security + timeout-minutes: 15 + + steps: + - name: Checkout release code + uses: actions/checkout@v4 + + - name: Build production container + run: docker build --target production --tag resume-matcher:release . + + - name: Run container + run: docker run -d --name resume-matcher-release -p 8080:8080 resume-matcher:release + + - name: Verify container health endpoint + run: | + for attempt in {1..15}; do + if curl -fsS http://127.0.0.1:8080/health > /dev/null; then + echo "Release container health endpoint is reachable." + exit 0 + fi + sleep 2 + done + echo "Release container health endpoint did not become ready in time." + docker logs resume-matcher-release + exit 1 + + - name: Stop container + if: always() + run: docker rm -f resume-matcher-release + + docker-publish: + name: Publish Release Images + runs-on: ubuntu-latest + needs: + - release-docker-smoke + timeout-minutes: 20 + + steps: + - name: Checkout release code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY_IMAGE }} + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ github.ref_name }} + type=raw,value=latest + + - name: Build and push multi-platform images + uses: docker/build-push-action@v6 + with: + context: . + target: production + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy-vercel: + name: Deploy Release to Vercel Production + runs-on: ubuntu-latest + needs: + - docker-publish + timeout-minutes: 15 + environment: + name: production + + steps: + - name: Checkout release code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel environment + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Build Vercel output + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Deploy prebuilt output + id: deploy + run: | + deployment_url="$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})" + echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT" + echo "Deployment URL: $deployment_url" + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Verify production deployment + run: | + deployment_url="${{ steps.deploy.outputs.deployment_url }}" + vercel curl --deployment "$deployment_url" / --token=${{ secrets.VERCEL_TOKEN }} > /dev/null + vercel curl --deployment "$deployment_url" /health --token=${{ secrets.VERCEL_TOKEN }} > /dev/null + + create-release: + name: Publish GitHub Release + runs-on: ubuntu-latest + needs: + - deploy-vercel + steps: + - name: Create GitHub release entry + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + generate_release_notes: true + name: ${{ github.ref_name }} + body: | + Production release for MatchlyPro. + + Container images and Vercel production deployment were published successfully for this version. + + notify: + name: Release Notification Stage + runs-on: ubuntu-latest + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + needs: + - release-quality + - release-security + - release-docker-smoke + - docker-publish + - deploy-vercel + - create-release + if: always() + + steps: + - name: Compute release notification status + id: status + run: | + if [[ "${{ needs.release-quality.result }}" == "success" && \ + "${{ needs.release-security.result }}" == "success" && \ + "${{ needs.release-docker-smoke.result }}" == "success" && \ + "${{ needs.docker-publish.result }}" == "success" && \ + "${{ needs.deploy-vercel.result }}" == "success" && \ + "${{ needs.create-release.result }}" == "success" ]]; then + echo "state=success" >> "$GITHUB_OUTPUT" + echo "message=Release pipeline completed successfully." >> "$GITHUB_OUTPUT" + else + echo "state=failure" >> "$GITHUB_OUTPUT" + echo "message=Release pipeline failed. Production promotion stopped." >> "$GITHUB_OUTPUT" + fi + + - name: Send release email notification + if: always() && env.SMTP_USER != '' && env.SMTP_PASS != '' + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 587 + username: ${{ env.SMTP_USER }} + password: ${{ env.SMTP_PASS }} + subject: "[${{ steps.status.outputs.state }}] ${{ github.repository }} release ${{ github.ref_name }}" + to: ${{ env.SMTP_USER }} + from: GitHub Actions <${{ env.SMTP_USER }}> + body: | + Repository: ${{ github.repository }} + Release Tag: ${{ github.ref_name }} + Commit: ${{ github.sha }} + + Release Quality Gate: ${{ needs.release-quality.result }} + Release Security Checks: ${{ needs.release-security.result }} + Release Docker Smoke Test: ${{ needs.release-docker-smoke.result }} + Publish Release Images: ${{ needs.docker-publish.result }} + Vercel Production Deploy: ${{ needs.deploy-vercel.result }} + GitHub Release Publish: ${{ needs.create-release.result }} + + ${{ steps.status.outputs.message }} + Run URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.gitleaks.toml b/.gitleaks.toml index c11f6a6..a035dd4 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,81 +1,26 @@ -# Gitleaks configuration for Resume Matcher project -title = "Resume Matcher Secret Detection" -version = "2.0.0" +title = "MatchlyPro Secret Detection" -[[rules]] -id = "generic-api-key" -description = "Generic API Key" -regex = '''(?i)(?:api[_-]?key|apikey|secret)[\s]*[=:][\s]*["']?([a-z0-9_\-]{20,60})["']?''' -tags = ["key", "API"] - -[[rules]] -id = "github-pat" -description = "GitHub Personal Access Token" -regex = '''(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36}''' -tags = ["key", "GitHub"] - -[[rules]] -id = "aws-access-key-id" -description = "AWS Access Key ID" -regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' -tags = ["key", "AWS"] - -[[rules]] -id = "aws-secret-access-key" -description = "AWS Secret Access Key" -regex = '''(?i)aws[_-]?secret[_-]?access[_-]?key[\s]*[=:][\s]*["']?([a-z0-9/+]{40})["']?''' -tags = ["key", "AWS"] - -[[rules]] -id = "slack-token" -description = "Slack Token" -regex = '''(xox[pboa]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})''' -tags = ["key", "Slack"] +[extend] +useDefault = true [[rules]] id = "vercel-api-token" description = "Vercel API Token" -regex = '''[a-z0-9]{24}_[a-z0-9]{27}''' +regex = '''\b[a-z0-9]{24}_[a-z0-9]{27}\b''' tags = ["key", "Vercel"] -[[rules]] -id = "npm-access-token" -description = "NPM Access Token" -regex = '''npm_[a-zA-Z0-9]{36}''' -tags = ["key", "NPM"] - -[[rules]] -id = "stripe-api-key" -description = "Stripe API Key" -regex = '''(sk|pk)_(test|live)_[0-9a-zA-Z]{24,99}''' -tags = ["key", "Stripe"] - -[[rules]] -id = "email-address" -description = "Email Address (for notification purposes - allowlisted)" -regex = '''[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}''' -tags = ["email"] - -# Allowlist specific patterns -[[allowlist]] -description = "Allowlisted email for notifications" -regexes = [ - "muhammadhammad\\.020050@gmail\\.com" -] - -[[allowlist]] -description = "Example/test patterns" +[[allowlists]] +description = "Ignore gitleaks config and markdown documentation" paths = [ - "test/", - "examples/", - "*.test.*", - "*.spec.*" + '''^\.gitleaks\.toml$''', + '''^README\.md$''', + '''^CONTRIBUTING\.md$''', + '''^LICENSE$''' ] -[[allowlist]] -description = "Documentation files" +[[allowlists]] +description = "Ignore lockfiles and generated metadata that may trigger false positives" paths = [ - "README.md", - "*.txt", - "*.md" -] \ No newline at end of file + '''^package-lock\.json$''', + '''^node_modules/.*''' +] diff --git a/CICD_SETUP.md b/CICD_SETUP.md deleted file mode 100644 index 5bcae9d..0000000 --- a/CICD_SETUP.md +++ /dev/null @@ -1,48 +0,0 @@ -# Production CI/CD Setup - -This project now uses a gated production pipeline: - -`Quality Gate -> Security Checks -> Docker Smoke Test -> Docker Publish -> Vercel Production Deploy -> Notification Stage` - -The important behavior is: - -- `main` par push hote hi Vercel khud se direct deploy nahi karega -- deployment sirf GitHub Actions pipeline ke successful hone ke baad hogi -- Vercel deploy `vercel deploy --prebuilt --prod` se hota hai -- Docker image pehle locally smoke-test hoti hai, phir GHCR aur Docker Hub par push hoti hai - -## Required Repository Secrets - -Ye secrets workflow use karta hai: - -- `DOCKERHUB_USERNAME` -- `DOCKERHUB_TOKEN` -- `SNYK_TOKEN` -- `VERCEL_TOKEN` -- `VERCEL_ORG_ID` -- `VERCEL_PROJECT_ID` -- `SMTP_USER` -- `SMTP_PASS` - -## What Changed - -- `vercel.json` me `git.deploymentEnabled: false` add kiya gaya hai taa ke Vercel Git auto-deploy band rahe -- workflow ko production-style gated jobs me split kiya gaya hai -- fake/no-op test commands ki jagah real validation aur smoke checks add kiye gaye hain -- duplicate validation scripts remove karke Node-based reusable scripts add kiye gaye hain -- Dockerfile ko static-site optimized banaya gaya hai - -## Local Validation - -Local machine par ye commands chala sakte ho: - -```bash -npm ci -npm run ci:validate -docker build --target production -t resume-matcher:local . -docker run --rm -p 8080:8080 resume-matcher:local -``` - -## Vercel Note - -`vercel.json` auto deployments ko disable karta hai. Agar aapka Vercel project abhi bhi connected Git integration ke through deploy kar raha ho, to Vercel dashboard me project settings -> Git me ja kar verify kar lo ke automatic Git deployments off hon. CLI-based production deploy workflow already configured hai. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f2d134f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +## Branch Strategy + +- `main` is the production-ready branch +- make changes in a feature branch such as `feature/...`, `fix/...`, or `chore/...` +- open a pull request into `main` +- wait for CI to pass +- get review before merge + +## Deployment Policy + +- branch pushes and pull requests run CI only +- production deployment does not happen from normal code pushes +- production deployment happens only from a version tag such as `v1.0.0` +- version tags are intended to be created only after approved code is already merged into `main` + +## Release Flow + +1. Merge approved changes into `main` +2. Create a version tag like `v1.0.0` +3. Push the tag +4. The release workflow publishes container images, deploys to Vercel production, and creates the GitHub Release entry + +## Recommended GitHub Settings + +For full enforcement, enable these repository rules in GitHub Settings: + +- require a pull request before merging into `main` +- require status checks to pass before merging +- require review from code owners +- block force pushes to `main` +- restrict who can push directly to `main` diff --git a/Dockerfile b/Dockerfile index 2c94e26..04fb80a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ RUN apk add --no-cache curl COPY nginx.conf /etc/nginx/nginx.conf COPY index.html /usr/share/nginx/html/index.html COPY health.html /usr/share/nginx/html/health.html -COPY logo /usr/share/nginx/html/logo RUN chown -R nginx:nginx /usr/share/nginx/html /var/cache/nginx /var/log/nginx /etc/nginx/conf.d \ && touch /var/run/nginx.pid \ diff --git a/README.md b/README.md index 7ab2d5e..22d9449 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ This project is part of my portfolio and showcases end-to-end ownership: - Docker containerization with Nginx - production deployment on Vercel - CI/CD automation with GitHub Actions -- security checks with `npm audit` and Snyk +- security checks with `npm audit`, gitleaks, and Snyk +- release-based production promotion ## Live Project @@ -36,10 +37,11 @@ This is not just a frontend page. It is a complete portfolio project where I han - built the tool myself - deployed it to Vercel myself - containerized it with Docker myself -- configured production CI/CD myself -- integrated security scanning and release gates myself +- configured CI myself +- configured release automation myself +- controlled production promotion through tags/releases myself -If you are reviewing this repository for hiring or portfolio purposes, the repo demonstrates practical ownership across development, deployment, and operations. +If you are reviewing this repository for hiring or portfolio purposes, the repo demonstrates practical ownership across development, deployment, release management, and operations. ## Tech Stack @@ -51,8 +53,10 @@ If you are reviewing this repository for hiring or portfolio purposes, the repo - Docker - Docker Hub - GitHub Actions +- GitHub Releases - Vercel - Snyk +- gitleaks ## Application Highlights @@ -62,7 +66,7 @@ If you are reviewing this repository for hiring or portfolio purposes, the repo - weighted scoring logic - section-aware analysis - role-family insights -- risk detection for weak resume coverage +- ATS-oriented risk detection ### Frontend experience @@ -71,7 +75,7 @@ If you are reviewing this repository for hiring or portfolio purposes, the repo - drag-and-drop PDF upload - live character counters - session history using local storage -- copy/export/save utilities +- copy, export, and save utilities ### Privacy-first approach @@ -80,31 +84,64 @@ If you are reviewing this repository for hiring or portfolio purposes, the repo - no database - no user account requirement -## DevOps And Production Setup +## DevOps And Release Model -The project includes a real CI/CD flow instead of a basic demo pipeline. +The project uses a two-stage workflow that separates CI from production release. -### Current pipeline +### CI pipeline -`Quality Gate -> Security Checks -> Docker Smoke Test -> Publish Container Images -> Deploy to Vercel Production -> Notification Stage` +`Quality Gate -> Security Checks -> Docker Smoke Test -> Notification Stage` -### What the pipeline does +What CI does: +- runs on branches and pull requests - installs dependencies with `npm ci` - validates project structure - runs static smoke checks +- runs gitleaks - runs `npm audit` - runs a Snyk dependency scan - builds and smoke-tests the production Docker image -- publishes container images to GHCR and Docker Hub -- deploys to Vercel only after earlier gates succeed -- sends a notification stage summary at the end + +### Release pipeline + +`Release Quality Gate -> Release Security Checks -> Release Docker Smoke Test -> Publish Release Images -> Deploy Release to Vercel Production -> Publish GitHub Release -> Release Notification Stage` + +What release does: + +- runs only when a version tag such as `v1.0.0` is pushed +- publishes release images to GHCR and Docker Hub +- deploys the tagged version to Vercel production +- creates a GitHub Release entry automatically ### Deployment rule Vercel does not auto-deploy directly from Git pushes for this project. -The repository uses CLI-based deployment after pipeline success, and `vercel.json` disables automatic Git deployments for tighter release control. +The repository uses CLI-based deployment, and `vercel.json` disables automatic Git deployments for tighter release control. + +Normal pushes and pull requests never deploy production. + +Production deployment happens only when I create and push a version tag like `v1.0.0`. + +## Branch And Review Workflow + +The intended workflow is: + +1. Create a branch such as `feature/...`, `fix/...`, or `chore/...` +2. Open a pull request into `main` +3. Let CI pass +4. Review the changes +5. Merge into `main` +6. Create a release tag when the version is ready for production + +Repo-side controls included here: + +- `.github/CODEOWNERS` +- `.github/pull_request_template.md` +- `CONTRIBUTING.md` + +For strict PR-only enforcement, GitHub branch protection should also be enabled in repository settings. ## Containerization @@ -148,22 +185,39 @@ This runs: - project validation - static smoke testing +## Release Usage + +To publish a real production release: + +```bash +git checkout main +git pull origin main +git tag v1.0.0 +git push origin v1.0.0 +``` + +That tag will trigger the release workflow, publish the production deployment, and create the GitHub Release entry. + ## Project Structure ```text . +├─ .github/CODEOWNERS +├─ .github/pull_request_template.md ├─ .github/workflows/ci-cd.yml +├─ .github/workflows/release.yml +├─ CONTRIBUTING.md ├─ Dockerfile ├─ docker-compose.yml +├─ LICENSE ├─ nginx.conf -├─ vercel.json ├─ package.json +├─ vercel.json ├─ index.html ├─ health.html -├─ scripts/ -│ ├─ smoke-static-site.mjs -│ └─ validate-project.mjs -└─ logo/ +└─ scripts/ + ├─ smoke-static-site.mjs + └─ validate-project.mjs ``` ## Vercel Note @@ -183,10 +237,11 @@ Relevant references: This repository is in a strong portfolio-ready state for a static production app: - working production deployment -- gated CI/CD pipeline +- CI separated from release promotion +- release-based production deployment +- PR review ownership with CODEOWNERS - containerized runtime - security scanning integrated -- release flow separated from direct Vercel Git auto-deploy - health checks for container and deployment verification - clean documentation for reviewers @@ -195,18 +250,19 @@ This repository is in a strong portfolio-ready state for a static production app - there is no backend or persistent database because the tool is intentionally browser-first - screenshot availability inside Vercel dashboard is a platform-side generated-URL behavior, not an application outage - local Docker verification depends on Docker Engine being available on the machine running the checks +- GitHub branch protection still needs to be enabled in repository settings for strict PR-only enforcement ## Future Improvements - custom domain -- Lighthouse performance report badge +- Lighthouse performance reporting - automated accessibility audit in CI - Playwright end-to-end tests - Dependabot for dependency maintenance -- versioned releases and changelog +- merge queue and stricter branch protection rules ## Author Muhammad Hammad -Built, containerized, deployed, and automated by me as a portfolio project to demonstrate product engineering plus practical DevOps ownership. +Built, containerized, deployed, released, and automated by me as a portfolio project to demonstrate product engineering plus practical DevOps ownership. diff --git a/logo/logo.html b/logo/logo.html deleted file mode 100644 index ec642d6..0000000 --- a/logo/logo.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/logo/logo.png b/logo/logo.png deleted file mode 100644 index fb08516..0000000 Binary files a/logo/logo.png and /dev/null differ diff --git a/scripts/validate-project.mjs b/scripts/validate-project.mjs index 220c446..4a3354e 100644 --- a/scripts/validate-project.mjs +++ b/scripts/validate-project.mjs @@ -5,6 +5,8 @@ const root = process.cwd(); const requiredFiles = [ ".github/workflows/ci-cd.yml", + ".github/workflows/release.yml", + ".github/CODEOWNERS", "Dockerfile", "nginx.conf", "package.json", @@ -36,7 +38,8 @@ for (const file of requiredFiles) { const packageJson = readJson("package.json"); const vercelConfig = readJson("vercel.json"); -const workflow = readText(".github/workflows/ci-cd.yml"); +const ciWorkflow = readText(".github/workflows/ci-cd.yml"); +const releaseWorkflow = readText(".github/workflows/release.yml"); const indexHtml = readText("index.html"); const healthHtml = readText("health.html"); @@ -49,11 +52,16 @@ assert(vercelConfig.git?.deploymentEnabled === false, "vercel.json must disable assert(Array.isArray(vercelConfig.routes), "vercel.json must define routes for static hosting."); assert(vercelConfig.routes.some((route) => route.src === "/health"), "vercel.json must expose /health."); -assert(workflow.includes("deploy-vercel"), "Workflow must include a deploy-vercel job."); -assert(workflow.includes("docker-publish"), "Workflow must include a docker-publish job."); -assert(workflow.includes("SNYK_TOKEN"), "Workflow must reference the SNYK token."); -assert(workflow.includes("DOCKERHUB_USERNAME"), "Workflow must reference Docker Hub credentials."); -assert(workflow.includes("vercel deploy --prebuilt --prod"), "Workflow must use prebuilt Vercel deployments."); +assert(ciWorkflow.includes("Notification Stage"), "CI workflow must include a notification stage."); +assert(ciWorkflow.includes("gitleaks"), "CI workflow must include gitleaks scanning."); +assert(ciWorkflow.includes("SNYK_TOKEN"), "CI workflow must reference the SNYK token."); + +assert(releaseWorkflow.includes("docker-publish"), "Release workflow must include a docker-publish job."); +assert(releaseWorkflow.includes("deploy-vercel"), "Release workflow must include a deploy-vercel job."); +assert(releaseWorkflow.includes("softprops/action-gh-release"), "Release workflow must create a GitHub release."); +assert(releaseWorkflow.includes("DOCKERHUB_USERNAME"), "Release workflow must reference Docker Hub credentials."); +assert(releaseWorkflow.includes("vercel deploy --prebuilt --prod"), "Release workflow must use prebuilt Vercel deployments."); +assert(releaseWorkflow.includes('v*.*.*'), "Release workflow must trigger from version tags."); assert(indexHtml.includes(""), "index.html must contain a title tag."); assert(indexHtml.toLowerCase().includes("resume"), "index.html should still describe the resume matcher app.");