fix: mismatched button/Link in mobile menu; ci: replace repo homepage… #91
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 🚀 Deploy Static Next.js to GitHub Pages | |
| on: | |
| push: | |
| branches: ['**'] # Triggers on push to any branch | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| workflow_dispatch: # Allows manual triggering | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: 'pages-${{ github.head_ref || github.ref_name }}' | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-deploy: | |
| name: 🏗 Build & Deploy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: 🔍 Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: 🧹 Strip stale deployment markers from README | |
| if: github.event_name == 'push' | |
| run: | | |
| if [ -f README.md ] && grep -q '<!-- DEPLOYMENT-URL:START -->' README.md; then | |
| echo "Stripping stale deployment markers from README..." | |
| # Remove everything between markers (inclusive), handling duplicates | |
| sed -i '/<!-- DEPLOYMENT-URL:START -->/,/<!-- DEPLOYMENT-URL:END -->/d' README.md | |
| # Also clean up any git conflict markers that might be left over | |
| sed -i '/^<<<<<<</d; /^=======/d; /^>>>>>>>/d' README.md | |
| # Remove resulting double-blank-lines | |
| sed -i '/^$/N;/^\n$/d' README.md | |
| if ! git diff --quiet README.md; then | |
| git config user.name "github-actions" | |
| git config user.email "github-actions@github.com" | |
| git add README.md | |
| git commit -m "ci: clean stale deployment markers from README [skip ci]" | |
| git push | |
| echo "Cleaned up stale deployment markers." | |
| fi | |
| fi | |
| - name: 🔧 Set deployment variables | |
| id: vars | |
| run: | | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| # Custom domain means no repo name prefix in the URL path. | |
| # Set CUSTOM_DOMAIN to your domain, or leave empty to use github.io/<repo> URLs. | |
| CUSTOM_DOMAIN="dev.codebuilder.org" | |
| # Get the branch name (works for both push and PR events) | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BRANCH="${{ github.head_ref }}" | |
| else | |
| BRANCH="${{ github.ref_name }}" | |
| fi | |
| # Sanitize branch name for use in URL paths | |
| SAFE_BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g') | |
| if [ -n "$CUSTOM_DOMAIN" ]; then | |
| BASE_URL="https://${CUSTOM_DOMAIN}" | |
| else | |
| BASE_URL="https://${{ github.repository_owner }}.github.io/${REPO_NAME}" | |
| fi | |
| if [ "$BRANCH" = "main" ]; then | |
| if [ -n "$CUSTOM_DOMAIN" ]; then | |
| echo "base_path=" >> $GITHUB_OUTPUT | |
| else | |
| echo "base_path=/${REPO_NAME}" >> $GITHUB_OUTPUT | |
| fi | |
| echo "dest_dir=" >> $GITHUB_OUTPUT | |
| # keep_files must be true so preview/ directories are preserved | |
| echo "keep_files=true" >> $GITHUB_OUTPUT | |
| echo "is_main=true" >> $GITHUB_OUTPUT | |
| echo "preview_url=${BASE_URL}/" >> $GITHUB_OUTPUT | |
| else | |
| echo "base_path=/preview/${SAFE_BRANCH}" >> $GITHUB_OUTPUT | |
| echo "dest_dir=preview/${SAFE_BRANCH}" >> $GITHUB_OUTPUT | |
| echo "keep_files=true" >> $GITHUB_OUTPUT | |
| echo "preview_url=${BASE_URL}/preview/${SAFE_BRANCH}/" >> $GITHUB_OUTPUT | |
| fi | |
| echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
| echo "safe_branch=$SAFE_BRANCH" >> $GITHUB_OUTPUT | |
| - name: 🔎 Detect package manager | |
| id: detect-pm | |
| run: | | |
| if [ -f "pnpm-lock.yaml" ]; then | |
| echo "manager=pnpm" >> $GITHUB_OUTPUT | |
| echo "command=install" >> $GITHUB_OUTPUT | |
| echo "runner=pnpm exec" >> $GITHUB_OUTPUT | |
| else | |
| echo "manager=npm" >> $GITHUB_OUTPUT | |
| echo "command=ci" >> $GITHUB_OUTPUT | |
| echo "runner=npx --no-install" >> $GITHUB_OUTPUT | |
| fi | |
| - name: 📦 Install pnpm | |
| if: steps.detect-pm.outputs.manager == 'pnpm' | |
| run: npm install -g pnpm | |
| - name: ⚙️ Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| cache: ${{ steps.detect-pm.outputs.manager }} | |
| - name: 📦 Cache Next.js build | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ${{ github.workspace }}/.next/cache | |
| key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- | |
| - name: 🚫 Ephemerally delete server/api files | |
| env: | |
| STATIC_BUILD_REMOVE_PATHS: ${{ vars.STATIC_BUILD_REMOVE_PATHS }} | |
| run: | | |
| DEFAULT_PATHS="'src/app/blog/[slug]' 'src/app/invoice/[id]' src/app/api src/server src/proxy.ts 'src/app/jobs/[id]' 'src/app/[...not-found]' prisma.config.ts" | |
| PATHS="${STATIC_BUILD_REMOVE_PATHS:-$DEFAULT_PATHS}" | |
| echo "Deleting server/api files for static build..." | |
| echo "Paths: $PATHS" | |
| eval rm -rf $PATHS | |
| - name: 📥 Install dependencies | |
| run: ${{ steps.detect-pm.outputs.manager }} ${{ steps.detect-pm.outputs.command }} | |
| - name: 🏗 Generate Static Build | |
| env: | |
| NEXT_OUTPUT_MODE: export | |
| GITHUB_PAGES: 1 | |
| NEXT_BASE_PATH: ${{ steps.vars.outputs.base_path }} | |
| run: | | |
| echo "Building static files for GitHub Pages..." | |
| echo "Base path: $NEXT_BASE_PATH" | |
| echo "Preview URL: ${{ steps.vars.outputs.preview_url }}" | |
| pnpm build | |
| touch out/.nojekyll | |
| - name: 🧹 Clean stale root files from gh-pages (main only) | |
| if: steps.vars.outputs.is_main == 'true' | |
| run: | | |
| # Checkout the gh-pages branch into a temp directory | |
| git fetch origin gh-pages || true | |
| mkdir -p /tmp/gh-pages-current | |
| cd /tmp/gh-pages-current | |
| git init | |
| git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git | |
| git fetch origin gh-pages --depth=1 || exit 0 | |
| git checkout gh-pages || exit 0 | |
| # Delete everything EXCEPT the preview/ directory and CNAME | |
| find . -maxdepth 1 ! -name '.' ! -name '.git' ! -name 'preview' ! -name 'CNAME' -exec rm -rf {} + | |
| git add -A | |
| git diff --cached --quiet || git -c user.name="github-actions" -c user.email="github-actions@github.com" commit -m "clean stale root files before main deploy" | |
| git push origin gh-pages || true | |
| - name: 🚀 Deploy to GitHub Pages | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./out | |
| destination_dir: ${{ steps.vars.outputs.dest_dir }} | |
| keep_files: ${{ steps.vars.outputs.keep_files }} | |
| cname: dev.codebuilder.org | |
| - name: 💬 Comment preview URL on PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const url = '${{ steps.vars.outputs.preview_url }}'; | |
| const sha = context.sha.substring(0, 7); | |
| const body = `🚀 **Preview deployment ready!**\n\n📎 **Preview URL:** ${url}\n\n_Deployed from commit \`${sha}\`_`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(c => c.body.includes('Preview deployment ready!')); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: 📝 Update README with deployment URL | |
| if: github.event_name == 'push' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const branch = '${{ steps.vars.outputs.branch }}'; | |
| const previewUrl = '${{ steps.vars.outputs.preview_url }}'; | |
| const sha = context.sha.substring(0, 7); | |
| const date = new Date().toISOString().split('T')[0]; | |
| // Fetch current README from the source branch | |
| let readmeData; | |
| try { | |
| readmeData = await github.rest.repos.getContent({ | |
| owner, repo, path: 'README.md', ref: branch, | |
| }); | |
| } catch (e) { | |
| console.log('No README.md found, skipping update.'); | |
| return; | |
| } | |
| const content = Buffer.from(readmeData.data.content, 'base64').toString('utf8'); | |
| const marker = { | |
| start: '<!-- DEPLOYMENT-URL:START -->', | |
| end: '<!-- DEPLOYMENT-URL:END -->', | |
| }; | |
| const section = [ | |
| marker.start, | |
| `> **🌐 Live Preview:** [${previewUrl}](${previewUrl})`, | |
| `> Deployed from \`${sha}\` on ${date}`, | |
| marker.end, | |
| ].join('\n'); | |
| let updated; | |
| if (content.includes(marker.start)) { | |
| // Replace existing section | |
| const re = new RegExp( | |
| `${marker.start}[\\s\\S]*?${marker.end}`, | |
| ); | |
| updated = content.replace(re, section); | |
| } else { | |
| // Insert after the first heading line | |
| const lines = content.split('\n'); | |
| const headingIdx = lines.findIndex(l => l.startsWith('# ')); | |
| if (headingIdx !== -1) { | |
| lines.splice(headingIdx + 1, 0, '', section, ''); | |
| updated = lines.join('\n'); | |
| } else { | |
| updated = section + '\n\n' + content; | |
| } | |
| } | |
| if (updated !== content) { | |
| await github.rest.repos.createOrUpdateFileContents({ | |
| owner, repo, path: 'README.md', | |
| message: `docs: update deployment URL for ${branch} [skip ci]`, | |
| content: Buffer.from(updated).toString('base64'), | |
| sha: readmeData.data.sha, | |
| branch, | |
| }); | |
| console.log(`README updated with preview URL: ${previewUrl}`); | |
| } | |
| - name: 📌 Update PR description with preview URL | |
| if: github.event_name == 'push' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const branch = '${{ steps.vars.outputs.branch }}'; | |
| const previewUrl = '${{ steps.vars.outputs.preview_url }}'; | |
| const sha = context.sha.substring(0, 7); | |
| // Find open PR for this branch | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner, repo, head: `${owner}:${branch}`, state: 'open', | |
| }); | |
| if (prs.length === 0) { | |
| console.log(`No open PR found for branch ${branch}, skipping.`); | |
| return; | |
| } | |
| const pr = prs[0]; | |
| const body = pr.body || ''; | |
| const marker = { | |
| start: '<!-- GH-PAGES-PREVIEW:START -->', | |
| end: '<!-- GH-PAGES-PREVIEW:END -->', | |
| }; | |
| const section = [ | |
| marker.start, | |
| `---`, | |
| `🌐 **Preview:** [${previewUrl}](${previewUrl}) `, | |
| `_Deployed from \`${sha}\`_`, | |
| `---`, | |
| marker.end, | |
| ].join('\n'); | |
| let updated; | |
| if (body.includes(marker.start)) { | |
| const re = new RegExp(`${marker.start}[\\s\\S]*?${marker.end}`); | |
| updated = body.replace(re, section); | |
| } else { | |
| // Prepend to top of PR description | |
| updated = section + '\n\n' + body; | |
| } | |
| if (updated !== body) { | |
| await github.rest.pulls.update({ | |
| owner, repo, pull_number: pr.number, body: updated, | |
| }); | |
| console.log(`PR #${pr.number} description updated with preview URL: ${previewUrl}`); | |
| } |