mirror of
https://github.com/mozilla/fxa.git
synced 2025-12-28 07:03:55 +00:00
feat(storybook): upload storybooks to github pages
Because: * we want to upload storybooks to github pages This commit: * sets up github actions for uploading storybooks to github pages Closes FXA-12782
This commit is contained in:
parent
cf8b195a16
commit
4562f7ab8e
61
.github/workflows/cleanup-storybooks.yml
vendored
Normal file
61
.github/workflows/cleanup-storybooks.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
name: Cleanup Storybooks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: gh-pages-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Remove closed PR storybooks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout gh-pages branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Remove PR directory
|
||||
run: |
|
||||
PR_DIR="storybooks/pr-${{ github.event.pull_request.number }}"
|
||||
if [ -d "gh-pages/$PR_DIR" ]; then
|
||||
rm -rf "gh-pages/$PR_DIR"
|
||||
echo "Removed $PR_DIR"
|
||||
else
|
||||
echo "Directory $PR_DIR not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
- name: Checkout main branch for scripts
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: main-repo
|
||||
|
||||
- name: Regenerate root index.html
|
||||
run: node main-repo/_scripts/generate-storybook-index.js
|
||||
env:
|
||||
GH_PAGES_DIR: gh-pages
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
|
||||
- name: Commit and push to gh-pages
|
||||
run: |
|
||||
cd gh-pages
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create orphan branch (no history) with current content
|
||||
git checkout --orphan gh-pages-new
|
||||
git add -A
|
||||
git commit -m "Remove storybooks for closed PR ${{ github.event.pull_request.number }}"
|
||||
|
||||
# Replace gh-pages with the new orphan branch
|
||||
git push --force origin gh-pages-new:gh-pages
|
||||
142
.github/workflows/deploy-storybooks.yml
vendored
Normal file
142
.github/workflows/deploy-storybooks.yml
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
name: Deploy Storybooks to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Storybooks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Clone l10n repository
|
||||
run: _scripts/l10n/clone.sh
|
||||
|
||||
- name: Build all Storybooks
|
||||
run: npx nx run-many -t build-storybook
|
||||
|
||||
- name: Organize Storybooks for deployment
|
||||
run: node _scripts/organize-storybooks.js
|
||||
env:
|
||||
DEPLOY_DIR: deploy/
|
||||
|
||||
- name: Upload storybooks artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybooks-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'main' }}
|
||||
path: deploy/
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
# Single concurrency group to prevent conflicts
|
||||
concurrency:
|
||||
group: gh-pages-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository for scripts
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
path: repo
|
||||
|
||||
- name: Checkout gh-pages branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set deployment directory
|
||||
id: deploy-dir
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
DEPLOY_DIR="storybooks/pr-${{ github.event.pull_request.number }}"
|
||||
else
|
||||
DEPLOY_DIR="storybooks/main"
|
||||
fi
|
||||
echo "path=$DEPLOY_DIR" >> $GITHUB_OUTPUT
|
||||
echo "DEPLOY_DIR=$DEPLOY_DIR" >> $GITHUB_ENV
|
||||
|
||||
- name: Remove old storybook directory
|
||||
run: rm -rf "gh-pages/${{ steps.deploy-dir.outputs.path }}"
|
||||
|
||||
- name: Download storybooks artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybooks-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'main' }}
|
||||
path: gh-pages/${{ steps.deploy-dir.outputs.path }}
|
||||
|
||||
- name: Generate root index.html
|
||||
run: node repo/_scripts/generate-storybook-index.js
|
||||
env:
|
||||
STORYBOOKS_DIR: gh-pages/storybooks
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
|
||||
- name: Commit and push to gh-pages
|
||||
run: |
|
||||
cd gh-pages
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
COMMIT_MSG="Deploy storybooks for PR ${{ github.event.pull_request.number }}"
|
||||
else
|
||||
COMMIT_MSG="Deploy storybooks from main branch"
|
||||
fi
|
||||
|
||||
# Create orphan branch (no history) with current content
|
||||
git checkout --orphan gh-pages-new
|
||||
git add -A
|
||||
git commit -m "$COMMIT_MSG"
|
||||
|
||||
# Replace gh-pages with the new orphan branch
|
||||
git push --force origin gh-pages-new:gh-pages
|
||||
|
||||
- name: Create GitHub status check
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const deployDir = process.env.DEPLOY_DIR;
|
||||
const deployUrl = `https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${deployDir}/`;
|
||||
const sha = context.payload.pull_request.head.sha;
|
||||
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: sha,
|
||||
state: 'success',
|
||||
context: 'Link to Storybooks',
|
||||
description: 'Storybooks deployed',
|
||||
target_url: deployUrl
|
||||
});
|
||||
|
||||
console.log(`Created status check for ${sha} with URL: ${deployUrl}`);
|
||||
134
_scripts/generate-storybook-index.js
Normal file
134
_scripts/generate-storybook-index.js
Normal file
@ -0,0 +1,134 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const storybooksDir = process.env.STORYBOOKS_DIR || 'gh-pages/storybooks';
|
||||
const repoName = process.env.REPO_NAME;
|
||||
const repoOwner = process.env.REPO_OWNER;
|
||||
|
||||
if (!repoName || !repoOwner) {
|
||||
console.error(
|
||||
'Error: REPO_NAME and REPO_OWNER environment variables are required'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getDirectories(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((item) => fs.statSync(path.join(dir, item)).isDirectory())
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getMetadata(dir) {
|
||||
const metadataPath = path.join(dir, 'metadata.json');
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(metadataPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to parse metadata from ${metadataPath}:`,
|
||||
error.message
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function generatePrItem(pr) {
|
||||
const title = `#${escapeHtml(pr.number)}${pr.metadata?.summary ? ` ${escapeHtml(pr.metadata.summary)}` : ''}`;
|
||||
const meta = pr.metadata
|
||||
? `<div class="meta">${escapeHtml(pr.metadata.commit.substring(0, 7))} • ${pr.metadata.timestamp ? new Date(pr.metadata.timestamp).toLocaleString('en-US', { timeZoneName: 'short' }) : ''} • <a href="https://github.com/${escapeHtml(repoOwner)}/${escapeHtml(repoName)}/pull/${escapeHtml(pr.number)}" target="_blank">View PR</a></div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<li>
|
||||
<a href="./${escapeHtml(pr.dir)}/">${title}</a>
|
||||
${meta}
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
const directories = getDirectories(storybooksDir);
|
||||
const prs = directories
|
||||
.filter((dir) => dir.startsWith('pr-'))
|
||||
.map((dir) => {
|
||||
const prNumber = dir.replace('pr-', '');
|
||||
const metadata = getMetadata(path.join(storybooksDir, dir));
|
||||
return {
|
||||
number: prNumber,
|
||||
dir,
|
||||
metadata,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => parseInt(b.number) - parseInt(a.number));
|
||||
|
||||
const mainMetadata = getMetadata(path.join(storybooksDir, 'main'));
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Storybooks for ${escapeHtml(repoName)}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.2rem; margin-top: 2rem; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { margin: 0.75rem 0; }
|
||||
a { color: #0969da; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.meta { color: #666; font-size: 0.875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Storybooks for ${escapeHtml(repoName)}</h1>
|
||||
|
||||
<h2>Latest Main Branch</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="./main/">${mainMetadata?.summary ? escapeHtml(mainMetadata.summary) : 'Main Branch Storybooks'}</a>
|
||||
${mainMetadata ? `<div class="meta">${escapeHtml(mainMetadata.commit.substring(0, 7))} • ${mainMetadata.timestamp ? new Date(mainMetadata.timestamp).toLocaleString('en-US', { timeZoneName: 'short' }) : ''}</div>` : ''}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
${
|
||||
prs.length > 0
|
||||
? `
|
||||
<h2>Pull Requests (${prs.length})</h2>
|
||||
<ul>
|
||||
${prs.map(generatePrItem).join('\n')}
|
||||
</ul>
|
||||
`
|
||||
: '<p style="color: #666;">No open pull requests with storybooks deployments.</p>'
|
||||
}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const outputPath = path.join(storybooksDir, 'index.html');
|
||||
fs.writeFileSync(outputPath, html);
|
||||
console.log(`Generated storybooks/index.html with main + ${prs.length} PRs`);
|
||||
153
_scripts/organize-storybooks.js
Normal file
153
_scripts/organize-storybooks.js
Normal file
@ -0,0 +1,153 @@
|
||||
/* ThisSource Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const deployDir = process.env.DEPLOY_DIR;
|
||||
if (!deployDir) {
|
||||
console.error('Error: DEPLOY_DIR environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
|
||||
function getCommitMetadata() {
|
||||
try {
|
||||
const commit = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
||||
const summary = execSync('git log -1 --pretty=format:%s', {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const description = execSync('git log -1 --pretty=format:%b', {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const timestamp = execSync('git log -1 --pretty=format:%ct', {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
return {
|
||||
commit,
|
||||
summary,
|
||||
description,
|
||||
timestamp: parseInt(timestamp, 10) * 1000, // Convert to milliseconds
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get commit metadata:', error.message);
|
||||
return {
|
||||
commit: 'unknown',
|
||||
summary: '',
|
||||
description: '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const commitMetadata = getCommitMetadata();
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function findStorybookDirs(dir, results = []) {
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
if (item === 'storybook-static' && fs.statSync(fullPath).isDirectory()) {
|
||||
results.push(fullPath);
|
||||
} else if (
|
||||
fs.statSync(fullPath).isDirectory() &&
|
||||
item !== 'node_modules' &&
|
||||
item !== 'deploy'
|
||||
) {
|
||||
findStorybookDirs(fullPath, results);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const storybookDirs = findStorybookDirs('.');
|
||||
|
||||
for (const storybookDir of storybookDirs) {
|
||||
const packageName = path.basename(path.dirname(storybookDir));
|
||||
const targetDir = path.join(deployDir, packageName);
|
||||
|
||||
console.log(`Copying ${packageName} storybook...`);
|
||||
fs.cpSync(storybookDir, targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storybooks = fs
|
||||
.readdirSync(deployDir)
|
||||
.filter((item) => fs.statSync(path.join(deployDir, item)).isDirectory())
|
||||
.sort();
|
||||
|
||||
const title = `Storybooks for commit ${escapeHtml(commitMetadata.commit)}`;
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 { font-size: 1.5rem; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { margin: 0.75rem 0; }
|
||||
a { color: #0969da; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
dl { margin-top: 2rem; }
|
||||
dt { font-weight: 600; margin-top: 1rem; color: #333; }
|
||||
dd { margin: 0.25rem 0 0 0; color: #666; }
|
||||
pre { margin: 0; white-space: pre-wrap; font-family: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
<ul>
|
||||
${storybooks.map((name) => `<li><a href="./${encodeURIComponent(name)}/">${escapeHtml(name)}</a></li>`).join('\n')}
|
||||
</ul>
|
||||
<dl>
|
||||
<dt>Date</dt>
|
||||
<dd>${escapeHtml(new Date(commitMetadata.timestamp).toLocaleString('en-US', { timeZoneName: 'short' }))}</dd>
|
||||
<dt>Summary</dt>
|
||||
<dd><pre>${escapeHtml(commitMetadata.summary)}</pre></dd>${
|
||||
commitMetadata.description
|
||||
? `
|
||||
<dt>Description</dt>
|
||||
<dd><pre>${escapeHtml(commitMetadata.description)}</pre></dd>`
|
||||
: ''
|
||||
}
|
||||
</dl>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(path.join(deployDir, 'index.html'), html);
|
||||
|
||||
// Write metadata as JSON for easier consumption by site index
|
||||
const metadata = {
|
||||
...commitMetadata,
|
||||
storybooks,
|
||||
deployDir,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(deployDir, 'metadata.json'),
|
||||
JSON.stringify(metadata, null, 2)
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Created index.html and metadata.json with ${storybooks.length} storybooks`
|
||||
);
|
||||
|
||||
console.log('\nDeployment structure:');
|
||||
console.log(storybooks.join('\n'));
|
||||
Loading…
Reference in New Issue
Block a user