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
7 changes: 7 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@

### Testing instructions

## Preview deployment

Check to deploy previews for this PR. Leave unchecked to skip.

- [ ] Deploy documentation preview
- [ ] Deploy Storybook preview

## Illustrations/Icons Checklist

Required if this PR changes files under `packages/illustrations/**` or `packages/icons/**`
Expand Down
58 changes: 58 additions & 0 deletions .github/scripts/remove-from-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node

/**
* Remove a PR preview entry from manifest.json
* Usage: node remove-from-manifest.mjs <pr-number>
*/

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Parse command line arguments
const [prNumber] = process.argv.slice(2);

if (!prNumber) {
console.error('Usage: node remove-from-manifest.mjs <pr-number>');
process.exit(1);
}

const manifestPath = path.join(process.cwd(), 'manifest.json');

// Read existing manifest
if (!fs.existsSync(manifestPath)) {
console.log('⚠️ Manifest does not exist, nothing to remove');
process.exit(0);
}

let manifest;
try {
const content = fs.readFileSync(manifestPath, 'utf-8');
manifest = JSON.parse(content);
} catch (error) {
console.error('❌ Failed to parse manifest:', error.message);
process.exit(1);
}

// Remove preview entry
const prNum = parseInt(prNumber, 10);
const initialLength = manifest.previews.length;
manifest.previews = manifest.previews.filter((p) => p.pr !== prNum);

if (manifest.previews.length === initialLength) {
console.log(`⚠️ PR #${prNum} not found in manifest`);
} else {
console.log(`✅ Removed preview for PR #${prNum}`);
}

// Update last updated timestamp
manifest.lastUpdated = new Date().toISOString();

// Write manifest
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');

console.log(`📝 Manifest updated successfully`);
console.log(` Total previews: ${manifest.previews.length}`);
88 changes: 88 additions & 0 deletions .github/scripts/update-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env node

/**
* Update manifest.json with a new or updated PR preview entry
* Usage: node update-manifest.mjs <pr-number> <pr-title> <branch> <author> <commit> <has-docs> <has-storybook>
*/

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Parse command line arguments
const [prNumber, prTitle, branch, author, commit, hasDocs, hasStorybook] = process.argv.slice(2);

if (!prNumber || !prTitle || !branch || !author || !commit) {
console.error(
'Usage: node update-manifest.mjs <pr-number> <pr-title> <branch> <author> <commit> <has-docs> <has-storybook>',
);
process.exit(1);
}

const manifestPath = path.join(process.cwd(), 'manifest.json');

// Read existing manifest or create new one
let manifest = {
previews: [],
lastUpdated: new Date().toISOString(),
};

if (fs.existsSync(manifestPath)) {
try {
const content = fs.readFileSync(manifestPath, 'utf-8');
manifest = JSON.parse(content);
} catch (error) {
console.warn('Failed to parse existing manifest, creating new one');
}
}

// Find existing preview or create new entry
const prNum = parseInt(prNumber, 10);
const existingIndex = manifest.previews.findIndex((p) => p.pr === prNum);

const previewEntry = {
pr: prNum,
title: prTitle,
branch: branch,
author: author,
baseUrl: `/cds/pr-${prNum}/`,
previews: {
docs: hasDocs === 'true' ? `/cds/pr-${prNum}/docs/` : null,
storybook: hasStorybook === 'true' ? `/cds/pr-${prNum}/storybook/` : null,
},
createdAt:
existingIndex >= 0 ? manifest.previews[existingIndex].createdAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
commit: commit,
};

if (existingIndex >= 0) {
// Update existing preview
manifest.previews[existingIndex] = previewEntry;
console.log(`✅ Updated preview for PR #${prNum}`);
} else {
// Add new preview
manifest.previews.push(previewEntry);
console.log(`✅ Added preview for PR #${prNum}`);
}

// Log which previews are available
const availablePreviews = [];
if (previewEntry.previews.docs) availablePreviews.push('docs');
if (previewEntry.previews.storybook) availablePreviews.push('storybook');
console.log(` Available previews: ${availablePreviews.join(', ') || 'none'}`);

// Sort by PR number (descending)
manifest.previews.sort((a, b) => b.pr - a.pr);

// Update last updated timestamp
manifest.lastUpdated = new Date().toISOString();

// Write manifest
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');

console.log(`📝 Manifest updated successfully`);
console.log(` Total PRs: ${manifest.previews.length}`);
125 changes: 125 additions & 0 deletions .github/workflows/preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Cleanup PR Preview

on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to clean up (for testing)'
required: true
type: number
pull_request:
types: [closed]

permissions:
contents: write
pull-requests: write

jobs:
cleanup:
name: Cleanup Preview
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
with:
egress-policy: audit

# Checkout main branch to get scripts
- name: Checkout main branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: master

# Get PR number
- name: Get PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "pr_number=${{ github.event.inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
fi

# Checkout gh-pages branch
- name: Checkout gh-pages
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: gh-pages
path: gh-pages-checkout
fetch-depth: 1

# Remove PR directory, update manifest, and push using an orphan commit so
# the branch history stays at depth 1 and doesn't accumulate binary blobs.
- name: Remove preview and push to gh-pages
working-directory: gh-pages-checkout
env:
PR_NUMBER: ${{ steps.pr-number.outputs.pr_number }}
run: |
set -e

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

apply_changes() {
PR_DIR="pr-${PR_NUMBER}"
if [ -d "$PR_DIR" ]; then
rm -rf "$PR_DIR"
echo "✅ Directory removed"
else
echo "⚠️ Directory does not exist: $PR_DIR"
fi
node ../.github/scripts/remove-from-manifest.mjs "$PR_NUMBER"
}

git fetch origin gh-pages
git reset --hard origin/gh-pages

apply_changes
git add -A

if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi

for i in 1 2 3; do
SQUASHED=$(git commit-tree HEAD^{tree} -m "Remove preview for PR #${PR_NUMBER}")
if git push --force-with-lease origin "${SQUASHED}:refs/heads/gh-pages"; then
echo "✅ Changes pushed to gh-pages"
exit 0
fi
echo "Push failed (concurrent update?), re-merging and retrying ($i/3)..."
sleep $((RANDOM % 10 + 1))
git fetch origin gh-pages
git reset --hard origin/gh-pages
apply_changes
git add -A
done

echo "❌ Failed to push after 3 retries"
exit 1

# Comment on PR
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const prNumber = ${{ steps.pr-number.outputs.pr_number }};

const comment = `## 🧹 Preview Cleaned Up

The documentation preview for this PR has been removed.

**Removed:** \`/cds/pr-${prNumber}/\`

---

🕐 Cleaned up at: \`${new Date().toISOString()}\``;

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: comment,
});
Loading
Loading