From ff700ebe70a889cbfd274bc4a33d28dbca8df202 Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Wed, 10 Jun 2026 13:28:53 +0530 Subject: [PATCH 1/3] ci: add PR previews, update guidelines, fix router basename - Migrate production deploy from actions/deploy-pages API to gh-pages branch via peaceiris/actions-gh-pages (preserves pr-preview/ directories) - Add new preview-pages.yml workflow for automated PR previews - Builds frontend with correct base path for subdirectory routing - Posts/updates PR comment with live preview URL - Cleans up preview directory when PR is closed or merged - Fix 12+ hardcoded absolute image paths (src='/...') to use import.meta.env.BASE_URL for correct resolution in subdirectories - Remove unnecessary \asename\ from HashRouter in App.jsx to prevent 404 routing issues in PR preview subdirectory deployments - Restructure CONTRIBUTING.md to establish clear General, Smart Contract, and Frontend contribution standards (including PR preview guidelines) --- .github/workflows/deploy.yml | 61 ++++--- .github/workflows/preview-pages.yml | 175 +++++++++++++++++++++ CONTRIBUTING.md | 52 ++++-- frontend/src/App.jsx | 2 +- frontend/src/components/InvoicePreview.jsx | 2 +- frontend/src/components/Navbar.jsx | 2 +- frontend/src/page/About.jsx | 2 +- frontend/src/page/BatchPayment.jsx | 2 +- frontend/src/page/Landing.jsx | 10 +- frontend/src/page/ReceivedInvoice.jsx | 2 +- frontend/src/page/SentInvoice.jsx | 2 +- frontend/src/utils/generateInvoicePDF.js | 7 +- 12 files changed, 268 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/preview-pages.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2452646a..dcfb6e20 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,25 +1,23 @@ -# Workflow for deploying static content to GitHub Pages +# Workflow for deploying static content to GitHub Pages via gh-pages branch. +# Preserves any PR preview directories that already exist on gh-pages. name: Deploy static content to Pages on: push: branches: ['main'] + paths: + - 'frontend/**' workflow_dispatch: permissions: - contents: read - pages: write - id-token: write + contents: write concurrency: - group: 'pages' - cancel-in-progress: true + group: 'gh-pages-deploy' + cancel-in-progress: false jobs: deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: # Checkout repo @@ -41,27 +39,46 @@ jobs: npm install fi working-directory: ./frontend + # Build project - name: Build run: npm run build working-directory: ./frontend - env: + env: VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} - # Setup Pages - - name: Setup Pages - uses: actions/configure-pages@v5 + # Restore PR preview directories from gh-pages branch (if present) + # so that a production redeploy does not wipe them out. + - name: Restore PR preview directories from gh-pages + run: | + git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { + echo "gh-pages branch does not exist yet — skipping restore." + exit 0 + } - # Upload artifact - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./frontend/dist + git fetch origin gh-pages --depth=1 - # Deploy to GitHub Pages - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + # Copy the pr-preview tree into the freshly built dist so the + # subsequent deploy action includes it in the push. + git checkout origin/gh-pages -- pr-preview/ 2>/dev/null && { + cp -r pr-preview ./frontend/dist/pr-preview + rm -rf pr-preview + } || { + echo "No pr-preview directory on gh-pages yet — skipping restore." + } + + # Add .nojekyll to prevent Jekyll processing + - name: Add .nojekyll + run: touch ./frontend/dist/.nojekyll + + # Deploy to gh-pages branch + - name: Deploy to gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + cname: chainvoice.stability.nexus + commit_message: "deploy: production site from ${{ github.sha }}" diff --git a/.github/workflows/preview-pages.yml b/.github/workflows/preview-pages.yml new file mode 100644 index 00000000..b1986ac4 --- /dev/null +++ b/.github/workflows/preview-pages.yml @@ -0,0 +1,175 @@ +# Builds and deploys the Vite frontend for PRs to the "pr-preview" directory +# on the gh-pages branch, and posts a comment with the preview URL to the PR. +# Also removes the preview from gh-pages when the PR is closed. + +name: Deploy PR preview to gh-pages + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + paths: + - 'frontend/**' + +permissions: + contents: write + pull-requests: write + +jobs: + # ─── Deploy preview on PR open / update ────────────────────────────────── + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + concurrency: + group: pr-preview-deploy-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: | + if [ -f "./frontend/package-lock.json" ]; then + npm ci + else + npm install + fi + working-directory: ./frontend + + - name: Build preview site + run: npm run build -- --base /pr-preview/pr-${{ github.event.pull_request.number }}/ + working-directory: ./frontend + env: + VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} + VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} + VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} + VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} + + - name: Add .nojekyll + run: touch ./frontend/dist/.nojekyll + + - name: Deploy preview to gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + destination_dir: pr-preview/pr-${{ github.event.pull_request.number }} + keep_files: true + commit_message: "deploy: preview for PR #${{ github.event.pull_request.number }} (${{ github.sha }})" + + - name: Post or update preview URL comment on PR + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const sha = context.sha.substring(0, 7); + const repoOwner = context.repo.owner; + const repoName = context.repo.repo; + + // Dynamically construct the preview environment URL. + // We use the default GitHub Pages domain format (https://.github.io/) + // because it universally supports both contributor forks and the main organization repository. + // If the main repository uses a custom domain (e.g., chainvoice.stability.nexus), + // GitHub Pages automatically redirects this standard URL to the custom domain. + const previewUrl = `https://${repoOwner}.github.io/${repoName}/pr-preview/pr-${prNumber}/`; + + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Preview URL** | ${previewUrl} |\n` + + `| **Commit** | \`${sha}\` |\n` + + `| **Status** | ✅ Deployed |\n\n` + + `> This preview is updated on every commit and will be removed when the PR is closed.`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: repoOwner, + repo: repoName, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + body, + }); + } + + # ─── Clean up preview when PR is closed (merged or abandoned) ──────────── + cleanup-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + concurrency: + group: pr-preview-cleanup-${{ github.event.pull_request.number }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Remove preview directory from gh-pages branch + run: | + # Guard: if gh-pages doesn't exist yet there is nothing to clean up. + git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { + echo "gh-pages branch does not exist — nothing to clean up." + exit 0 + } + + # Fetch and create a local tracking branch. + git fetch origin gh-pages:gh-pages --depth=1 + git checkout gh-pages + + PR_DIR="pr-preview/pr-${{ github.event.pull_request.number }}" + if [ -d "$PR_DIR" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rm -r -f "$PR_DIR" + git commit -m "cleanup: remove preview for PR #${{ github.event.pull_request.number }}" + git push --set-upstream origin gh-pages + else + echo "Preview directory '$PR_DIR' not found — nothing to clean up." + fi + + - name: Update PR comment to indicate removal + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Status** | 🗑️ Preview removed (PR closed) |\n`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06cb3e89..16b5f6bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,50 @@ image -## Smart Contract Contribution Guidelines -The following guidelines apply to all contributions that modify or introduce smart contract logic in ChainVoice. Since smart contracts form the core logic of the system, a higher standard of review and validation is required compared to frontend or general application changes. +## General Contribution & Review Process +The following guidelines apply to all contributions across the entire project (both smart contracts and frontend). + +**1. Pull Request Scope:** +Pull requests must remain focused and limited in scope. Each pull request should address a single feature, improvement, or bug fix. Large, multi-feature updates or broad refactors must be divided into smaller, logically structured changes before review to ensure clarity and effective feedback. + +**2. Build, Test & CI Requirements:** +Before opening a pull request, you must ensure that your local build is successful (`npm run build`), all relevant tests pass, and there are no new console errors or warnings in the browser. All automated CI checks must pass before requesting review. -**1. Introduction:** -Smart contract development in ChainVoice requires stricter standards than frontend or general application changes. Contract modifications involve compilation, logic validation, security review, and careful testing. Unlike UI updates, their impact is not immediately visible and often requires deployment and interaction to verify behavior. All contributors must adhere to the following guidelines to maintain code quality and security. +**3. Automated Code Review (CodeRabbit):** +All comments and suggestions raised by CodeRabbit must be carefully reviewed and addressed before requesting a mentor review. Pull requests with failing checks, console errors, or unresolved automated review comments will not be considered for manual review. -**2. Test Requirements:** +**4. Requesting a Review:** +Once your PR is ready and all CI checks pass, drop the PR link (along with the live preview URL, if applicable) in the [⁠Stability Nexus>#⁠Chainvoice](https://discord.com/channels/995968619034984528/1328282666335993856) Discord channel. Remember to include relevant demo screenshots or screen recordings showcasing your changes. + +--- + +## Smart Contract Contribution Guidelines +Since smart contracts form the core logic of the system, a higher standard of review and validation is required compared to frontend or general application changes. + +**1. Test Requirements:** Any pull request that modifies or introduces smart contract logic must include comprehensive automated tests. Tests must validate expected behavior, cover relevant edge cases, and properly test revert and failure scenarios. If existing logic is modified, corresponding tests must also be updated. Pull requests without sufficient test coverage will not be reviewed, as tests serve as the primary validation mechanism for contract correctness. -**3. Design and Architecture Approval:** +**2. Design and Architecture Approval:** For any major feature, architectural change, or significant contract modification, a detailed issue must be opened before implementation begins. The issue must clearly describe the proposed design, data structures, access control model, and expected behavior. Implementation may begin only after the approach has been discussed and approved to prevent architectural inconsistencies and unnecessary rework. -**4. Pull Request Scope:** -Pull requests must remain focused and limited in scope. Each pull request should address a single feature, improvement, or bug fix. Large, multi-feature updates or broad refactors must be divided into smaller, logically structured changes before review to ensure clarity and effective feedback. +**3. Review Standards:** +During review, emphasis will be placed on architectural soundness, correctness of state management, access control implementation, event emission consistency, potential security risks such as reentrancy or improper validation, and overall contract design quality. Where relevant, gas efficiency and upgrade safety will also be considered. -**5. Continuous Integration and Automated Review Requirements:** -All automated checks must pass before requesting review. This includes successful compilation, linting, and execution of all tests. If continuous integration fails, contributors are required to resolve the issues before requesting further review. ```In addition, all comments and suggestions raised by CodeRabbit must be carefully reviewed and addressed before requesting a mentor review.``` Pull requests with unresolved automated review comments will not be considered for manual review. Review time should focus on logic, architecture, and security considerations rather than basic correctness issues. +--- -**6. Review Standards:** -During review, emphasis will be placed on architectural soundness, correctness of state management, access control implementation, event emission consistency, potential security risks such as reentrancy or improper validation, and overall contract design quality. Where relevant, gas efficiency and upgrade safety will also be considered. +## Frontend Contribution Guidelines +The following guidelines apply to all contributions that modify or introduce frontend changes in ChainVoice. + +**1. Static Asset Paths:** +All static file references (images, icons, fonts, etc.) must use `import.meta.env.BASE_URL` instead of hardcoded absolute paths. This ensures assets resolve correctly in both the production site and automated PR preview deployments. +```jsx +// ✅ Correct + + +// ❌ Incorrect + +``` -Please ensure that all future contract-related contributions follow this process. If there are any questions or clarifications needed, feel free to raise them before proceeding with implementation. +**2. PR Preview Deployments:** +When a pull request modifies files inside `frontend/`, a live preview is automatically deployed to GitHub Pages. A bot comment with the preview URL will be posted on your PR. The preview is cleaned up automatically when the PR is closed or merged. -Thank you for your cooperation and continued contributions. +Thank you for your cooperation and continued contributions. \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d5d15d9c..137b8415 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -81,7 +81,7 @@ function App() { })} >
- + }> } /> diff --git a/frontend/src/components/InvoicePreview.jsx b/frontend/src/components/InvoicePreview.jsx index 3e7e895d..148a0f86 100644 --- a/frontend/src/components/InvoicePreview.jsx +++ b/frontend/src/components/InvoicePreview.jsx @@ -68,7 +68,7 @@ const InvoicePreview = ({
Chainvoice { diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 74ac36ed..b57e9f2f 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -149,7 +149,7 @@ function Navbar() { closeMobileMenu(); }} > - logo + logo

Chainvoice

diff --git a/frontend/src/page/About.jsx b/frontend/src/page/About.jsx index 7fec7b51..19225d6d 100644 --- a/frontend/src/page/About.jsx +++ b/frontend/src/page/About.jsx @@ -467,7 +467,7 @@ function About() { rel="noopener noreferrer" > Stability Nexus diff --git a/frontend/src/page/BatchPayment.jsx b/frontend/src/page/BatchPayment.jsx index 524524e6..faee3f0c 100644 --- a/frontend/src/page/BatchPayment.jsx +++ b/frontend/src/page/BatchPayment.jsx @@ -1312,7 +1312,7 @@ function BatchPayment() {
- Chainvoice + Chainvoice

Cha diff --git a/frontend/src/page/Landing.jsx b/frontend/src/page/Landing.jsx index cde39c8c..dd32f840 100644 --- a/frontend/src/page/Landing.jsx +++ b/frontend/src/page/Landing.jsx @@ -39,7 +39,7 @@ function Landing() {

Lit Protocol @@ -65,14 +65,14 @@ function Landing() { className="relative" > Secure Invoice Dashboard
Lit Protocol @@ -210,7 +210,7 @@ function Landing() { className="relative" > Token Payment Flow @@ -268,7 +268,7 @@ function Landing() { rel="noopener noreferrer" > Stability Nexus diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 70912444..31f76d38 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -1638,7 +1638,7 @@ function ReceivedInvoice() {
Chainvoice { diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index e5278469..b4aff652 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -732,7 +732,7 @@ function SentInvoice() {
Chainvoice { diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index b4197936..5408d2e0 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -13,7 +13,8 @@ import { const loadLogoImage = async () => { try { const invoiceElement = document.getElementById("invoice-print"); - const logoImg = invoiceElement?.querySelector('img[src="/logo.png"]') || + const logoPath = `${import.meta.env.BASE_URL}logo.png`; + const logoImg = invoiceElement?.querySelector(`img[src="${logoPath}"]`) || invoiceElement?.querySelector('img[src*="logo"]'); if (logoImg) { @@ -52,7 +53,7 @@ const loadLogoImage = async () => { } try { - const response = await fetch('/logo.png', { + const response = await fetch(`${import.meta.env.BASE_URL}logo.png`, { method: 'GET', headers: { 'Cache-Control': 'no-cache', @@ -143,7 +144,7 @@ const loadLogoImage = async () => { reject(new Error('Image load failed')); }; - img.src = "/logo.png"; + img.src = `${import.meta.env.BASE_URL}logo.png`; }); return logoDataUrl; From ddc4823ab3e55bee3279d37318e74e429224e2bf Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Thu, 11 Jun 2026 15:28:18 +0530 Subject: [PATCH 2/3] ci: fix PR preview 403 by splitting into fork-safe two-workflow architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original single-workflow PR preview (preview-pages.yml) failed on the StabilityNexus org with 'Permission denied to github-actions[bot]' because org-level settings restrict GITHUB_TOKEN to read-only. Split into two workflows: - pr-build.yml: runs on pull_request with read-only permissions (fork-safe), uploads only metadata — no build, no secrets needed. - pr-deploy.yml: runs on workflow_run in base repo context with write access. Checks out PR code, builds with secrets (wallet connect works in previews), deploys to gh-pages, and comments preview URL on the PR. Security: - pr-deploy.yml always runs from main — forks cannot modify deploy logic - PR code checked out with persist-credentials: false - GITHUB_TOKEN only passed to peaceiris deploy action, never to build steps - Metadata validated (PR number, action, SHA) to prevent injection Cleanup: - On PR close/merge: removes pr-preview/pr-N/ from gh-pages - Updates PR comment to indicate preview removal Production impact: None. deploy.yml restores pr-preview/ dirs before deploying, and PR previews use keep_files: true. Requires org admin to enable 'Read and write permissions' for GITHUB_TOKEN at Settings > Actions > General > Workflow permissions. --- .github/workflows/pr-build.yml | 66 +++++++ .github/workflows/pr-deploy.yml | 271 ++++++++++++++++++++++++++++ .github/workflows/preview-pages.yml | 175 ------------------ 3 files changed, 337 insertions(+), 175 deletions(-) create mode 100644 .github/workflows/pr-build.yml create mode 100644 .github/workflows/pr-deploy.yml delete mode 100644 .github/workflows/preview-pages.yml diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000..b4055b6a --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,66 @@ +# Workflow 1 of 2 for PR previews (fork-safe). +# This is a lightweight trigger — it only uploads metadata. +# The actual build and deploy is handled by pr-deploy.yml via workflow_run, +# which runs in the base repository context with secrets and write permissions. +# +# WHY TWO WORKFLOWS? +# 1. Single workflow with `pull_request` + `contents: write` fails with 403 +# on org repos where GITHUB_TOKEN defaults to read-only. +# 2. Fork PRs never get secrets on `pull_request` trigger (GitHub blocks this). +# By building in pr-deploy.yml (workflow_run context), we get secrets so +# features like wallet connection work in PR previews. +# 3. Fork code never gets write access — pr-deploy.yml always runs from main. + +name: "PR Preview: Build" + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + paths: + - 'frontend/**' + +# Read-only permissions — this workflow only uploads metadata. +permissions: + contents: read + +jobs: + # ─── Signal a deploy when PR is opened or updated ─────────────────────── + signal-deploy: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + concurrency: + group: pr-preview-build-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + - name: Save deploy metadata + run: | + mkdir -p ./pr-metadata + echo "${{ github.event.pull_request.number }}" > ./pr-metadata/pr_number + echo "${{ github.event.pull_request.head.sha }}" > ./pr-metadata/sha + echo "deploy" > ./pr-metadata/action + + - name: Upload PR metadata + uses: actions/upload-artifact@v4 + with: + name: pr-metadata-${{ github.event.pull_request.number }} + path: ./pr-metadata + retention-days: 1 + + # ─── Signal cleanup when PR is closed (merged or abandoned) ───────────── + signal-cleanup: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Save cleanup metadata + run: | + mkdir -p ./pr-metadata + echo "${{ github.event.pull_request.number }}" > ./pr-metadata/pr_number + echo "${{ github.event.pull_request.head.sha }}" > ./pr-metadata/sha + echo "cleanup" > ./pr-metadata/action + + - name: Upload PR metadata + uses: actions/upload-artifact@v4 + with: + name: pr-metadata-${{ github.event.pull_request.number }} + path: ./pr-metadata + retention-days: 1 diff --git a/.github/workflows/pr-deploy.yml b/.github/workflows/pr-deploy.yml new file mode 100644 index 00000000..d1be53ed --- /dev/null +++ b/.github/workflows/pr-deploy.yml @@ -0,0 +1,271 @@ +# Workflow 2 of 2 for PR previews (fork-safe). +# Triggered by workflow_run after pr-build.yml completes. +# Runs in the BASE REPOSITORY context — has write permissions and secrets, +# so it can build with VITE_ env vars, push to gh-pages, and comment on PRs. +# +# SECURITY NOTES: +# - This workflow ALWAYS runs the code from the default branch (main), +# so fork PRs cannot modify the deploy/cleanup logic. +# - PR source code is checked out with persist-credentials: false, +# so build scripts cannot access GITHUB_TOKEN via git. +# - GITHUB_TOKEN is only passed to the peaceiris deploy action, never to build steps. +# +# PREREQUISITE (org admin action required): +# Go to github.com/StabilityNexus/Chainvoice/settings/actions +# → Workflow permissions → select "Read and write permissions" +# +# PRODUCTION IMPACT: None. +# - PR previews deploy to pr-preview/pr-N/ subdirectory only. +# - Production deploy.yml restores pr-preview/ dirs before deploying. +# - Different concurrency groups prevent conflicts. + +name: "PR Preview: Deploy" + +on: + workflow_run: + workflows: ["PR Preview: Build"] + types: [completed] + +permissions: + contents: write + pull-requests: write + +jobs: + deploy-preview: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + concurrency: + group: pr-preview-deploy + cancel-in-progress: false + steps: + # ─── Download the metadata artifact to determine action ────────────── + - name: Download PR metadata + uses: actions/github-script@v7 + id: metadata + with: + script: | + // List artifacts from the triggering workflow run + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + // Find the metadata artifact + const metadataArtifact = artifacts.data.artifacts.find( + a => a.name.startsWith('pr-metadata-') + ); + if (!metadataArtifact) { + core.setFailed('No PR metadata artifact found'); + return; + } + + // Download and extract metadata + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: metadataArtifact.id, + archive_format: 'zip', + }); + + const fs = require('fs'); + fs.mkdirSync('./pr-metadata', { recursive: true }); + fs.writeFileSync('./pr-metadata.zip', Buffer.from(download.data)); + + - name: Extract and validate metadata + run: | + unzip -o ./pr-metadata.zip -d ./pr-metadata + + PR_NUMBER=$(cat ./pr-metadata/pr_number | tr -d '[:space:]') + PR_SHA=$(cat ./pr-metadata/sha | tr -d '[:space:]') + PR_ACTION=$(cat ./pr-metadata/action | tr -d '[:space:]') + + # Validate PR number is a positive integer (prevents injection) + if ! echo "$PR_NUMBER" | grep -qE '^[0-9]+$'; then + echo "::error::Invalid PR number: '$PR_NUMBER'" + exit 1 + fi + + # Validate action is one of the expected values + if [ "$PR_ACTION" != "deploy" ] && [ "$PR_ACTION" != "cleanup" ]; then + echo "::error::Invalid action: '$PR_ACTION' (expected 'deploy' or 'cleanup')" + exit 1 + fi + + # Validate SHA looks like a hex commit hash + if ! echo "$PR_SHA" | grep -qE '^[0-9a-f]{40}$'; then + echo "::error::Invalid SHA: '$PR_SHA'" + exit 1 + fi + + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + echo "PR_SHA=$PR_SHA" >> $GITHUB_ENV + echo "PR_ACTION=$PR_ACTION" >> $GITHUB_ENV + echo "✅ Metadata validated: PR #$PR_NUMBER, action=$PR_ACTION, sha=${PR_SHA:0:7}" + + # ─── Deploy preview (when action == deploy) ───────────────────────── + + # Checkout PR code with NO credentials persisted — build scripts + # cannot access GITHUB_TOKEN through git commands. + - name: Checkout PR code + if: env.PR_ACTION == 'deploy' + uses: actions/checkout@v4 + with: + ref: ${{ env.PR_SHA }} + persist-credentials: false + + - name: Setup Node + if: env.PR_ACTION == 'deploy' + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + if: env.PR_ACTION == 'deploy' + run: | + if [ -f "./frontend/package-lock.json" ]; then + npm ci + else + npm install + fi + working-directory: ./frontend + + - name: Build preview site + if: env.PR_ACTION == 'deploy' + run: npm run build -- --base /${{ github.event.workflow_run.repository.name }}/pr-preview/pr-${{ env.PR_NUMBER }}/ + working-directory: ./frontend + env: + # Secrets are available here because this workflow runs in the base + # repo context. This allows wallet connection to work in PR previews. + # Note: GITHUB_TOKEN is intentionally NOT passed to the build step. + VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} + VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} + VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} + VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} + + - name: Add .nojekyll + if: env.PR_ACTION == 'deploy' + run: touch ./frontend/dist/.nojekyll + + - name: Deploy preview to gh-pages + if: env.PR_ACTION == 'deploy' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + destination_dir: pr-preview/pr-${{ env.PR_NUMBER }} + keep_files: true + user_name: "github-actions[bot]" + user_email: "github-actions[bot]@users.noreply.github.com" + commit_message: "deploy: preview for PR #${{ env.PR_NUMBER }} (${{ env.PR_SHA }})" + + - name: Post or update deploy comment on PR + if: env.PR_ACTION == 'deploy' + uses: actions/github-script@v7 + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER); + const sha = process.env.PR_SHA.substring(0, 7); + const repoOwner = context.repo.owner; + const repoName = context.repo.repo; + + // Dynamically construct the preview environment URL. + // We use the default GitHub Pages domain format (https://.github.io/) + // because it universally supports both contributor forks and the main organization repository. + // If the main repository uses a custom domain (e.g., chainvoice.stability.nexus), + // GitHub Pages automatically redirects this standard URL to the custom domain. + const previewUrl = `https://${repoOwner}.github.io/${repoName}/pr-preview/pr-${prNumber}/`; + + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Preview URL** | ${previewUrl} |\n` + + `| **Commit** | \`${sha}\` |\n` + + `| **Status** | ✅ Deployed |\n\n` + + `> This preview is updated on every commit and will be removed when the PR is closed.`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: repoOwner, + repo: repoName, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + body, + }); + } + + # ─── Cleanup preview (when action == cleanup) ────────────────────── + - name: Checkout repository + if: env.PR_ACTION == 'cleanup' + uses: actions/checkout@v4 + + - name: Remove preview from gh-pages + if: env.PR_ACTION == 'cleanup' + run: | + # Guard: if gh-pages doesn't exist yet there is nothing to clean up. + git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { + echo "gh-pages branch does not exist — nothing to clean up." + exit 0 + } + + # Fetch and create a local tracking branch. + git fetch origin gh-pages:gh-pages --depth=1 + git checkout gh-pages + + PR_DIR="pr-preview/pr-${{ env.PR_NUMBER }}" + if [ -d "$PR_DIR" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rm -r -f "$PR_DIR" + git commit -m "cleanup: remove preview for PR #${{ env.PR_NUMBER }}" + git push --set-upstream origin gh-pages + echo "✅ Preview directory '$PR_DIR' removed from gh-pages." + else + echo "Preview directory '$PR_DIR' not found — nothing to clean up." + fi + + - name: Update PR comment to indicate removal + if: env.PR_ACTION == 'cleanup' + uses: actions/github-script@v7 + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER); + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Status** | 🗑️ Preview removed (PR closed) |\n`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } diff --git a/.github/workflows/preview-pages.yml b/.github/workflows/preview-pages.yml deleted file mode 100644 index b1986ac4..00000000 --- a/.github/workflows/preview-pages.yml +++ /dev/null @@ -1,175 +0,0 @@ -# Builds and deploys the Vite frontend for PRs to the "pr-preview" directory -# on the gh-pages branch, and posts a comment with the preview URL to the PR. -# Also removes the preview from gh-pages when the PR is closed. - -name: Deploy PR preview to gh-pages - -on: - pull_request: - types: [opened, synchronize, reopened, closed] - paths: - - 'frontend/**' - -permissions: - contents: write - pull-requests: write - -jobs: - # ─── Deploy preview on PR open / update ────────────────────────────────── - deploy-preview: - if: github.event.action != 'closed' - runs-on: ubuntu-latest - concurrency: - group: pr-preview-deploy-${{ github.event.pull_request.number }} - cancel-in-progress: true - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install dependencies - run: | - if [ -f "./frontend/package-lock.json" ]; then - npm ci - else - npm install - fi - working-directory: ./frontend - - - name: Build preview site - run: npm run build -- --base /pr-preview/pr-${{ github.event.pull_request.number }}/ - working-directory: ./frontend - env: - VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} - VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} - VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} - VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} - - - name: Add .nojekyll - run: touch ./frontend/dist/.nojekyll - - - name: Deploy preview to gh-pages branch - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./frontend/dist - destination_dir: pr-preview/pr-${{ github.event.pull_request.number }} - keep_files: true - commit_message: "deploy: preview for PR #${{ github.event.pull_request.number }} (${{ github.sha }})" - - - name: Post or update preview URL comment on PR - uses: actions/github-script@v7 - with: - script: | - const prNumber = context.payload.pull_request.number; - const sha = context.sha.substring(0, 7); - const repoOwner = context.repo.owner; - const repoName = context.repo.repo; - - // Dynamically construct the preview environment URL. - // We use the default GitHub Pages domain format (https://.github.io/) - // because it universally supports both contributor forks and the main organization repository. - // If the main repository uses a custom domain (e.g., chainvoice.stability.nexus), - // GitHub Pages automatically redirects this standard URL to the custom domain. - const previewUrl = `https://${repoOwner}.github.io/${repoName}/pr-preview/pr-${prNumber}/`; - - const marker = ''; - const body = - `${marker}\n` + - `### 🔍 PR Preview\n\n` + - `| | |\n` + - `|---|---|\n` + - `| **Preview URL** | ${previewUrl} |\n` + - `| **Commit** | \`${sha}\` |\n` + - `| **Status** | ✅ Deployed |\n\n` + - `> This preview is updated on every commit and will be removed when the PR is closed.`; - - const { data: comments } = await github.rest.issues.listComments({ - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - }); - - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: repoOwner, - repo: repoName, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - body, - }); - } - - # ─── Clean up preview when PR is closed (merged or abandoned) ──────────── - cleanup-preview: - if: github.event.action == 'closed' - runs-on: ubuntu-latest - concurrency: - group: pr-preview-cleanup-${{ github.event.pull_request.number }} - cancel-in-progress: false - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Remove preview directory from gh-pages branch - run: | - # Guard: if gh-pages doesn't exist yet there is nothing to clean up. - git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { - echo "gh-pages branch does not exist — nothing to clean up." - exit 0 - } - - # Fetch and create a local tracking branch. - git fetch origin gh-pages:gh-pages --depth=1 - git checkout gh-pages - - PR_DIR="pr-preview/pr-${{ github.event.pull_request.number }}" - if [ -d "$PR_DIR" ]; then - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git rm -r -f "$PR_DIR" - git commit -m "cleanup: remove preview for PR #${{ github.event.pull_request.number }}" - git push --set-upstream origin gh-pages - else - echo "Preview directory '$PR_DIR' not found — nothing to clean up." - fi - - - name: Update PR comment to indicate removal - uses: actions/github-script@v7 - with: - script: | - const prNumber = context.payload.pull_request.number; - const marker = ''; - const body = - `${marker}\n` + - `### 🔍 PR Preview\n\n` + - `| | |\n` + - `|---|---|\n` + - `| **Status** | 🗑️ Preview removed (PR closed) |\n`; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - }); - - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } From 705677dd097b1e89480f99c981f019cfc6485b3d Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Thu, 11 Jun 2026 16:51:48 +0530 Subject: [PATCH 3/3] ci: add explanatory comment for workflow permissions --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dcfb6e20..fb7dbbd3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,7 @@ on: workflow_dispatch: permissions: + # Required by peaceiris/actions-gh-pages to push built site to the gh-pages branch contents: write concurrency: