Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions .github/workflows/cleanup-previews.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Cleanup Stale PR Previews

on:
schedule:
# Run every day at 2:00 AM UTC
- cron: '0 2 * * *'
workflow_dispatch: # Allow manual trigger

permissions:
contents: write
pull-requests: read

jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages branch
uses: actions/checkout@v4
with:
ref: gh-pages
fetch-depth: 0

- name: Clean up stale preview directories
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// Configuration
const RETENTION_DAYS = 7; // Keep previews for PRs closed within this many days

// Helper function to safely extract error information
function extractErrorInfo(error) {
const status = (typeof error === 'object' && error !== null && 'status' in error)
? error.status
: undefined;
const message = (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string')
? error.message
: String(error);
return { status, message };
}

// Get all open and recently closed PRs (with pagination)
const openPRs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});

const closedPRs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
per_page: 100,
sort: 'updated',
direction: 'desc'
});

// Keep previews for open PRs and PRs closed in the last N days
const retentionMs = RETENTION_DAYS * 24 * 60 * 60 * 1000;
const cutoffDate = new Date(Date.now() - retentionMs);
const activePRNumbers = new Set([
...openPRs.map(pr => pr.number),
...closedPRs
.filter(pr => pr.closed_at && new Date(pr.closed_at) > cutoffDate)
.map(pr => pr.number)
]);

console.log(`Retention period: ${RETENTION_DAYS} days`);
console.log(`Active PR numbers to keep: ${Array.from(activePRNumbers).join(', ')}`);

// Check if pr-preview directory exists
const previewDir = 'pr-preview';
if (!fs.existsSync(previewDir)) {
console.log('No pr-preview directory found, nothing to clean up');
return;
}

// List all preview directories
const previewDirs = fs.readdirSync(previewDir)
.filter(dir => dir.startsWith('pr-'))
.map(dir => {
const match = dir.match(/^pr-(\d+)$/);
return match ? { dir, prNumber: parseInt(match[1]) } : null;
})
.filter(x => x !== null);

console.log(`Found ${previewDirs.length} preview directories`);

// Identify stale directories
const staleDirectories = previewDirs.filter(
({ prNumber }) => !activePRNumbers.has(prNumber)
);

if (staleDirectories.length === 0) {
console.log('No stale preview directories to remove');
return;
}

console.log(`Found ${staleDirectories.length} stale preview directories to remove:`);
staleDirectories.forEach(({ dir, prNumber }) => {
console.log(` - ${dir} (PR #${prNumber})`);
});

// Remove stale directories
staleDirectories.forEach(({ dir }) => {
const dirPath = path.join(previewDir, dir);
try {
execSync(`rm -rf "${dirPath}"`, { stdio: 'inherit' });
console.log(`Removed: ${dirPath}`);
} catch (error) {
const { message } = extractErrorInfo(error);
console.error(`Failed to remove preview directory: ${dirPath}`, message);
throw error;
}
});

// Commit and push changes
try {
execSync('git config --local user.name "github-actions[bot]"', { stdio: 'inherit' });
execSync('git config --local user.email "github-actions[bot]@users.noreply.github.com"', { stdio: 'inherit' });
console.log('Git config set successfully');
} catch (error) {
const { message } = extractErrorInfo(error);
console.error('Failed to configure git:', message);
throw error;
}

try {
execSync('git add pr-preview', { stdio: 'inherit' });
console.log('Changes staged successfully');
} catch (error) {
const { message } = extractErrorInfo(error);
console.error('Failed to stage changes:', message);
throw error;
}

try {
execSync(`git commit -m "chore: cleanup ${staleDirectories.length} stale PR preview(s)"`, { stdio: 'inherit' });
console.log('Changes committed successfully');
} catch (error) {
const { message } = extractErrorInfo(error);
if (message && message.includes('nothing to commit')) {
console.log('No changes to commit');
return;
}
console.error('Failed to commit changes:', message);
throw error;
}

try {
execSync('git push', { stdio: 'inherit' });
console.log('Changes pushed successfully');
} catch (error) {
const { message } = extractErrorInfo(error);
console.error('Failed to push changes:', message);
throw error;
}
7 changes: 5 additions & 2 deletions .github/workflows/jekyll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ permissions:
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
group: "production-deploy"
cancel-in-progress: false

jobs:
build-and-deploy:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -48,5 +48,8 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
# keep_files is intentionally set to true so that existing content (including PR preview directories)
# is not deleted on each deploy. Old preview directories are periodically cleaned up by
# .github/workflows/cleanup-previews.yml to prevent unbounded growth of the gh-pages branch.
keep_files: true
publish_branch: gh-pages
28 changes: 25 additions & 3 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ permissions:
pull-requests: write

concurrency:
group: preview-${{ github.ref }}
group: preview-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -142,8 +142,30 @@ jobs:
currentBody = currentBody.substring(0, markerIndex).trim();
}

const previewSection = `## Alternate language PR\n${alternateLangLink}\n\n---\n\n## 🚀 PR Preview\n\n| Name | Link(s) |\n|------|------|\n| **Latest commit** | [${context.payload.pull_request.head.sha.substring(0, 7)}](${commitUrl}) |\n| **Page preview** | ${previewLinks} |${otherFilesContent ? `\n| **Other changed files** | ${otherFilesContent} |` : ''}\n\n---\n*<small>Preview updates automatically with each commit*</small>`;

const otherFilesRow = otherFilesContent
? `| **Other changed files** | ${otherFilesContent} |`
: null;

const previewLines = [
'',
'## Alternate language PR',
alternateLangLink,
'',
'---',
'',
'## 🚀 PR Preview',
'',
'| Name | Link(s) |',
'|------|----------|',
`| **Latest commit** | [${context.payload.pull_request.head.sha.substring(0, 7)}](${commitUrl}) |`,
`| **Page preview** | ${previewLinks} |`,
...(otherFilesRow ? [otherFilesRow] : []),
'',
'---',
'*<small>Preview updates automatically with each commit*</small>',
];

const previewSection = previewLines.join('\n');
// Update PR description
await github.rest.pulls.update({
owner: context.repo.owner,
Expand Down