diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 31c93c96c9..1222557832 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -108,14 +108,91 @@ jobs: HAPPO_API_KEY: ${{ secrets.HAPPO_API_KEY }} HAPPO_API_SECRET: ${{ secrets.HAPPO_API_SECRET }} - deploy-docs: + deploy-picasso-docs: if: ${{ github.event.pull_request.head.ref != 'changeset-release/master' }} name: Deploy Picasso docs runs-on: ubuntu-latest + concurrency: + group: gh-pages-deployment + cancel-in-progress: false permissions: - contents: read + contents: write + pull-requests: write + pages: write + id-token: write + needs: [static-checks] steps: - name: Checkout uses: actions/checkout@v4 - # Some new deployment process is needed here + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.18 + + - name: Install dependencies from cache + uses: ./.github/actions/yarn-install + + - name: Build Storybook + run: yarn build:storybook + + - name: Checkout existing gh-pages content + id: checkout-gh-pages + uses: actions/checkout@v4 + continue-on-error: true + with: + ref: gh-pages + path: gh-pages-content + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create gh-pages directory if branch doesn't exist + if: steps.checkout-gh-pages.outcome == 'failure' + run: | + echo "๐Ÿ†• gh-pages branch doesn't exist, creating organized directory structure" + mkdir -p gh-pages-content/prs + echo "# Picasso Storybook" > gh-pages-content/README.md + echo "This directory contains Storybook deployments for Picasso." >> gh-pages-content/README.md + echo "" >> gh-pages-content/README.md + echo "- Production: Root directory" >> gh-pages-content/README.md + echo "- PR Previews: prs/{number}/" >> gh-pages-content/README.md + + - name: Prepare preview deployment + run: | + # Create prs/{number} directory in organized structure + PR_DIR="gh-pages-content/prs/${{ github.event.pull_request.number }}" + + # Remove existing PR directory if it exists + rm -rf "$PR_DIR" + + # Create new PR directory and copy storybook build + mkdir -p "$PR_DIR" + cp -r build/storybook/* "$PR_DIR/" + + echo "๐Ÿ“‚ Prepared preview in: $PR_DIR" + ls -la "$PR_DIR" + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: gh-pages-content + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const prNumber = '${{ github.event.pull_request.number }}'; + const deployUrl = `https://toptal.github.io/picasso/prs/${prNumber}/`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '๐Ÿ“– **Storybook Preview**\n\n๐Ÿš€ Your Storybook preview is ready: **[View Storybook](' + deployUrl + ')**\n\n๐Ÿ“ Preview URL: `' + deployUrl + '`\n\nThis preview is updated automatically when you push changes to this PR.' + }); diff --git a/.github/workflows/cleanup-previews.yml b/.github/workflows/cleanup-previews.yml new file mode 100644 index 0000000000..9fc29b9058 --- /dev/null +++ b/.github/workflows/cleanup-previews.yml @@ -0,0 +1,175 @@ +name: Cleanup PR Previews + +on: + # Immediate cleanup when PR closes + pull_request: + types: [closed] + branches: + - master + - 'feature/**' + + # Scheduled garbage collection (runs weekly on Sundays at 2 AM UTC) + schedule: + - cron: '0 2 * * 0' + + # Manual trigger for emergency cleanup + workflow_dispatch: + inputs: + cleanup_mode: + description: 'Cleanup mode' + required: true + default: 'single' + type: choice + options: + - single + - all_closed + - force_all + +concurrency: + group: gh-pages-deployment + cancel-in-progress: false + +jobs: + cleanup-single-pr: + if: ${{ github.event_name == 'pull_request' }} + name: Cleanup Single PR Preview + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + pages: write + id-token: write + steps: + - name: Checkout gh-pages branch + id: checkout-gh-pages + uses: actions/checkout@v4 + continue-on-error: true + with: + ref: gh-pages + path: gh-pages-content + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove PR preview directory + if: steps.checkout-gh-pages.outcome == 'success' + run: | + PR_DIR="gh-pages-content/prs/${{ github.event.pull_request.number }}" + if [ -d "$PR_DIR" ]; then + echo "๐Ÿ—‘๏ธ Removing preview directory: $PR_DIR" + rm -rf "$PR_DIR" + echo "โœ… Cleanup successful" + else + echo "โ„น๏ธ Preview directory $PR_DIR does not exist" + fi + + - name: Deploy cleanup to GitHub Pages + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/upload-pages-artifact@v3 + with: + path: gh-pages-content + + - name: Deploy to GitHub Pages + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/deploy-pages@v4 + + - name: Comment cleanup notification + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '๐Ÿ—‘๏ธ **Storybook preview cleaned up**\n\nThe preview deployment has been automatically removed since this PR was closed.' + }); + + garbage-collection: + if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + name: Garbage Collection + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Checkout gh-pages branch + id: checkout-gh-pages + uses: actions/checkout@v4 + continue-on-error: true + with: + ref: gh-pages + path: gh-pages-content + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Garbage collect orphaned previews + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Get all open PRs + const { data: openPRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open' + }); + + const openPRNumbers = new Set(openPRs.map(pr => pr.number.toString())); + console.log(`๐Ÿ“Š Found ${openPRNumbers.size} open PRs:`, Array.from(openPRNumbers).join(', ')); + + // Check prs directory + const prsDir = 'gh-pages-content/prs'; + if (!fs.existsSync(prsDir)) { + console.log('โ„น๏ธ No prs directory found'); + return; + } + + const existingPreviews = fs.readdirSync(prsDir); + console.log(`๐Ÿ“ Found ${existingPreviews.length} preview directories:`, existingPreviews.join(', ')); + + let cleanedCount = 0; + for (const prDir of existingPreviews) { + const prNumber = prDir; + + // Skip if PR is still open (unless force mode) + if (openPRNumbers.has(prNumber) && '${{ github.event.inputs.cleanup_mode }}' !== 'force_all') { + console.log(`โญ๏ธ Skipping ${prNumber} (PR still open)`); + continue; + } + + // Remove closed PR preview + const fullPath = path.join(prsDir, prDir); + try { + fs.rmSync(fullPath, { recursive: true, force: true }); + console.log(`๐Ÿ—‘๏ธ Cleaned up preview: ${prNumber}`); + cleanedCount++; + } catch (error) { + console.error(`โŒ Failed to clean ${prNumber}:`, error.message); + } + } + + console.log(`โœ… Garbage collection complete: ${cleanedCount} previews cleaned`); + + // Set output for summary + core.setOutput('cleaned_count', cleanedCount); + core.setOutput('total_found', existingPreviews.length); + + - name: Deploy cleanup to GitHub Pages + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/upload-pages-artifact@v3 + with: + path: gh-pages-content + + - name: Deploy to GitHub Pages + if: steps.checkout-gh-pages.outcome == 'success' + uses: actions/deploy-pages@v4 + + - name: Summary + run: | + echo "๐Ÿงน **Garbage Collection Summary**" >> $GITHUB_STEP_SUMMARY + echo "- Trigger: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a79d7538ec..a336ab4bdb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,15 @@ jobs: release: name: Release runs-on: ubuntu-latest + concurrency: + group: gh-pages-deployment + cancel-in-progress: false permissions: contents: write issues: write pull-requests: write + pages: write + id-token: write steps: - name: Checkout Repo uses: actions/checkout@v4 @@ -136,7 +141,82 @@ jobs: payload: | text: 'A new PR was merged to Picasso :parrotspin:' - # Implement Storybook deployment here + - name: Build Storybook for Production + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + run: | + yarn build:storybook + echo "๐Ÿ“– Building Storybook for production deployment..." >> $GITHUB_STEP_SUMMARY + + - name: Checkout existing gh-pages content + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + id: checkout-gh-pages-release + uses: actions/checkout@v4 + continue-on-error: true + with: + ref: gh-pages + path: gh-pages-content + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create gh-pages directory if branch doesn't exist + if: ${{ success() && steps.changesets.outputs.published == 'true' && steps.checkout-gh-pages-release.outcome == 'failure' }} + run: | + echo "๐Ÿ†• gh-pages branch doesn't exist, creating organized directory structure" + mkdir -p gh-pages-content/prs + echo "# Picasso Storybook" > gh-pages-content/README.md + echo "This directory contains Storybook deployments for Picasso." >> gh-pages-content/README.md + echo "" >> gh-pages-content/README.md + echo "- Production: Root directory" >> gh-pages-content/README.md + echo "- PR Previews: prs/{number}/" >> gh-pages-content/README.md + + - name: Prepare production deployment (preserve PR previews) + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + run: | + # Preserve prs directory if it exists + if [ -d "gh-pages-content/prs" ]; then + echo "๐Ÿ“ Backing up prs directory" + mv gh-pages-content/prs ./prs-backup + fi + + # Clear root content but preserve .git and prs + find gh-pages-content -mindepth 1 -maxdepth 1 -not -name ".git" -not -name "prs" -exec rm -rf {} + + + # Deploy production to root + echo "๐Ÿ“ฆ Deploying production Storybook to root" + cp -r build/storybook/* gh-pages-content/ + + # Restore prs + if [ -d "./prs-backup" ]; then + echo "๐Ÿ”„ Restoring prs directory" + mv ./prs-backup gh-pages-content/prs + fi + + echo "๐Ÿ“‚ Production deployment prepared with preserved previews" + echo "Root content:" + ls -la gh-pages-content/ + echo "PR previews content:" + ls -la gh-pages-content/prs/ 2>/dev/null || echo "No prs directory" + + - name: Setup Pages + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + uses: actions/configure-pages@v4 + + - name: Upload artifact + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + uses: actions/upload-pages-artifact@v3 + with: + path: gh-pages-content + + - name: Deploy Production Storybook to GitHub Pages + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + id: storybook-deployment + uses: actions/deploy-pages@v4 + + - name: Storybook Deployment Success + if: ${{ success() && steps.changesets.outputs.published == 'true' }} + run: | + echo "๐Ÿš€ Production Storybook deployed to GitHub Pages!" >> $GITHUB_STEP_SUMMARY + echo "๐Ÿ“– Visit: https://toptal.github.io/picasso/" >> $GITHUB_STEP_SUMMARY + echo "โœ… PR previews preserved during production deployment" >> $GITHUB_STEP_SUMMARY integration-tests: name: Integration Tests