Skip to content

Commit 32b5e0f

Browse files
committed
[code-infra] Automate team sync on about page
1 parent 3078e63 commit 32b5e0f

2 files changed

Lines changed: 152 additions & 7 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Sync team members
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
reviewers:
7+
description: 'Comma-separated GitHub usernames to request review from'
8+
required: false
9+
default: 'oliviertassinari'
10+
schedule:
11+
# Every Monday at 06:00 UTC
12+
- cron: '0 6 * * 1'
13+
14+
permissions: {}
15+
16+
jobs:
17+
sync:
18+
if: github.repository == 'mui/material-ui'
19+
runs-on: ubuntu-latest
20+
permissions:
21+
contents: write
22+
pull-requests: write
23+
steps:
24+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
25+
- name: Set up pnpm
26+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
27+
- name: Use Node.js
28+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
29+
with:
30+
node-version: '22.18.0'
31+
cache: 'pnpm' # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-dependencies
32+
- run: pnpm install
33+
- name: Sync team members
34+
run: pnpm docs:sync-team
35+
env:
36+
# syncTeamMembers.ts writes the missing-images summary for the PR body here.
37+
SYNC_SUMMARY_FILE: ${{ runner.temp }}/sync-summary.md
38+
- name: Create or update pull request
39+
env:
40+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
SYNC_SUMMARY_FILE: ${{ runner.temp }}/sync-summary.md
42+
# Empty on scheduled runs, so fall back to the default reviewers.
43+
REVIEWERS: ${{ inputs.reviewers || 'oliviertassinari' }}
44+
run: |
45+
set -euo pipefail
46+
if [ -z "$(git status --porcelain)" ]; then
47+
echo 'No team member changes. Skipping PR.'
48+
exit 0
49+
fi
50+
51+
BRANCH=website/sync-team-members
52+
git config user.name 'github-actions[bot]'
53+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
54+
git switch -C "$BRANCH"
55+
git commit --all --message '[website] Sync about page team members'
56+
git push --force origin "$BRANCH"
57+
58+
BODY="$(printf 'Automated sync of the About page team members from the [mui-about API](https://frontend-public.mui.com/api/mui-about).\n\n'; cat "$SYNC_SUMMARY_FILE")"
59+
60+
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
61+
gh pr edit "$BRANCH" --body "$BODY"
62+
else
63+
gh pr create \
64+
--base master \
65+
--head "$BRANCH" \
66+
--title '[website] Sync about page team members' \
67+
--body "$BODY" \
68+
--label website \
69+
--reviewer "$REVIEWERS"
70+
fi

docs/scripts/syncTeamMembers.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,92 @@ import url from 'url';
33
import * as fs from 'fs/promises';
44
import path from 'path';
55

6+
interface Person {
7+
name: string;
8+
}
9+
10+
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
11+
const teamMembersPath = path.resolve(currentDirectory, '../data/about/teamMembers.json');
12+
const imagesDirectory = path.resolve(currentDirectory, '../public/static/branding/about');
13+
14+
// Mirror the image filename derivation used in docs/src/components/about/Team.tsx.
15+
function imageFileName(name: string) {
16+
return `${name
17+
.split(' ')
18+
.map((part) => part.toLowerCase())
19+
.join('-')}.png`;
20+
}
21+
22+
async function fileExists(filePath: string) {
23+
try {
24+
await fs.access(filePath);
25+
return true;
26+
} catch {
27+
return false;
28+
}
29+
}
30+
31+
async function readCurrentMembers(): Promise<Person[]> {
32+
try {
33+
return JSON.parse(await fs.readFile(teamMembersPath, 'utf8'));
34+
} catch {
35+
return [];
36+
}
37+
}
38+
39+
// Delete images of removed members and report new members missing a photo.
40+
async function syncImagesAndSummary(previousMembers: Person[], people: Person[], ciMode: boolean) {
41+
const previousNames = new Set(previousMembers.map((person) => person.name));
42+
const nextNames = new Set(people.map((person) => person.name));
43+
const added = people.filter((person) => !previousNames.has(person.name));
44+
const removed = previousMembers.filter((person) => !nextNames.has(person.name));
45+
46+
// Delete images of members who are no longer part of the team.
47+
await Promise.all(
48+
removed.map(async (person) => {
49+
const imagePath = path.join(imagesDirectory, imageFileName(person.name));
50+
if (await fileExists(imagePath)) {
51+
await fs.unlink(imagePath);
52+
}
53+
}),
54+
);
55+
56+
// Flag new members whose image still needs to be added manually.
57+
const missingImages = (
58+
await Promise.all(
59+
added.map(async (person) => {
60+
const fileName = imageFileName(person.name);
61+
const exists = await fileExists(path.join(imagesDirectory, fileName));
62+
return exists ? null : `${person.name} (\`${fileName}\`)`;
63+
}),
64+
)
65+
).filter((item): item is string => item !== null);
66+
67+
const summary = missingImages.length
68+
? `### ⚠️ Missing images\n\nAdd a photo for the following new members to \`docs/public/static/branding/about/\`:\n\n${missingImages
69+
.map((entry) => `- ${entry}`)
70+
.join('\n')}`
71+
: '';
72+
73+
if (summary) {
74+
if (ciMode) {
75+
await fs.writeFile(process.env.SYNC_SUMMARY_FILE as string, summary, 'utf8');
76+
} else {
77+
console.log(summary);
78+
}
79+
}
80+
}
81+
682
async function run() {
83+
// Read the current roster before overwriting, only when a diff is needed.
84+
const previousMembers = process.env.SYNC_SUMMARY_FILE ? await readCurrentMembers() : [];
85+
786
const response = await fetch('https://frontend-public.mui.com/api/mui-about');
8-
const { people } = await response.json();
87+
const { people }: { people: Person[] } = await response.json();
988

10-
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url));
89+
await fs.writeFile(teamMembersPath, JSON.stringify(people), 'utf8');
1190

12-
await fs.writeFile(
13-
path.resolve(currentDirectory, '../data/about/teamMembers.json'),
14-
JSON.stringify(people),
15-
'utf8',
16-
);
91+
await syncImagesAndSummary(previousMembers, people, !!process.env.SYNC_SUMMARY_FILE);
1792

1893
console.log('done');
1994
}

0 commit comments

Comments
 (0)