Skip to content
Closed
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}`);
121 changes: 121 additions & 0 deletions .github/workflows/preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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

# Pull latest so concurrent cleanups/deploys don't overwrite each other
- name: Pull latest gh-pages
working-directory: gh-pages-checkout
run: git pull origin gh-pages

# Remove PR directory
- name: Remove preview directory
run: |
PR_DIR="gh-pages-checkout/pr-${{ steps.pr-number.outputs.pr_number }}"

if [ -d "$PR_DIR" ]; then
echo "Removing preview directory: $PR_DIR"
rm -rf "$PR_DIR"
echo "✅ Directory removed"
else
echo "⚠️ Directory does not exist: $PR_DIR"
fi

# Update manifest
- name: Update manifest
working-directory: gh-pages-checkout
run: |
node ../.github/scripts/remove-from-manifest.mjs "${{ steps.pr-number.outputs.pr_number }}"

# Commit and push to gh-pages (retry on race with concurrent deploy/cleanup)
- name: Commit changes
working-directory: gh-pages-checkout
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git add .
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Remove preview for PR #${{ steps.pr-number.outputs.pr_number }}"
for i in 1 2 3; do
if git push origin gh-pages; then
echo "✅ Changes pushed to gh-pages"
exit 0
fi
echo "Push failed (concurrent update?), pulling and retrying ($i/3)..."
git pull origin gh-pages --rebase
done
echo "❌ Failed to push after retries"
exit 1
fi

# 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