Skip to content
Merged
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
122 changes: 72 additions & 50 deletions .github/scripts/a11y-pr-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,65 @@ export function readPrA11yRoutes(repoRoot) {
}));
}

/**
* The public components each changed multi-component folder (such as Chat or
* Table) implements, from the analysis's unresolved component sources. Only
* the changed folder expands, never the whole package roster. Shared by the
* accessibility scope and the touched-components check.
*
* @param {string[]} sources - `package/Folder` entries
* @param {string} [repoRoot]
* @returns {{packageName: string, directory: string, name: string, owner: string}[]}
*/
export function changedFolderMembers(sources, repoRoot = process.cwd()) {
const members = [];
const publicComponentsByPackage = new Map();
for (const source of sources) {
const [packageName, directory, extra] = source.split('/');
const pkg = componentPackage(packageName);
if (
!pkg ||
extra !== undefined ||
!/^[A-Za-z0-9_.-]+$/.test(directory ?? '') ||
['.', '..'].includes(directory)
) {
throw new Error(`Invalid unresolved component source: ${source}`);
}
const sourceRoot = path.join(repoRoot, pkg.src);
const changedDirectory = path.join(sourceRoot, directory);
const names = documentedComponentNames(changedDirectory);
if (names.length === 0) continue;
let publicComponents = publicComponentsByPackage.get(packageName);
if (publicComponents == null) {
publicComponents = new Set(
pkg.layout === 'flat'
? flatPackageComponentNames(repoRoot, pkg)
: nestedPackageComponentNames(repoRoot, pkg),
);
publicComponentsByPackage.set(packageName, publicComponents);
}
const exports = componentExportsFromBarrel(
path.join(sourceRoot, 'index.ts'),
sourceRoot,
);
for (const name of names) {
// Utility docs in shared source directories do not create component owners.
if (!publicComponents.has(name)) continue;
// A doc can mention a dependency such as Avatar; it does not make that
// component part of the changed folder's scope.
if (exports.get(name)?.startsWith(`${changedDirectory}${path.sep}`)) {
members.push({
packageName,
directory,
name,
owner: `${packageName}/${name}`,
});
}
}
}
return members;
}

export function resolvePrA11yComponents(
analysis,
repoRoot = process.cwd(),
Expand Down Expand Up @@ -104,56 +163,19 @@ export function resolvePrA11yComponents(
}
// Analysis deliberately leaves multi-component folders (such as Chat)
// unresolved. Expand only that changed folder, not the whole package roster.
const publicComponentsByPackage = new Map();
for (const source of analysis.unresolvedComponentSources) {
const [packageName, directory, extra] = source.split('/');
const pkg = componentPackage(packageName);
if (
!pkg ||
extra !== undefined ||
!/^[A-Za-z0-9_.-]+$/.test(directory ?? '') ||
['.', '..'].includes(directory)
) {
throw new Error(`Invalid unresolved component source: ${source}`);
}
const sourceRoot = path.join(repoRoot, pkg.src);
const changedDirectory = path.join(sourceRoot, directory);
const names = documentedComponentNames(changedDirectory);
if (names.length === 0) continue;
let publicComponents = publicComponentsByPackage.get(packageName);
if (publicComponents == null) {
publicComponents = new Set(
pkg.layout === 'flat'
? flatPackageComponentNames(repoRoot, pkg)
: nestedPackageComponentNames(repoRoot, pkg),
);
publicComponentsByPackage.set(packageName, publicComponents);
}
const exports = componentExportsFromBarrel(
path.join(sourceRoot, 'index.ts'),
sourceRoot,
);
for (const name of names) {
// Utility docs in shared source directories do not create component owners.
if (!publicComponents.has(name)) continue;
const implementation = exports.get(name);
// A doc can mention a dependency such as Avatar; it does not make that
// component part of the changed folder's scope.
if (implementation?.startsWith(`${changedDirectory}${path.sep}`)) {
routes ??= readPrA11yRoutes(repoRoot);
const owner = `${packageName}/${name}`;
const umbrella = `${packageName}/${directory}`;
if (componentRoutesForFilters(routes, [owner]).length > 0) {
owners.add(owner);
} else if (componentRoutesForFilters(routes, [umbrella]).length > 0) {
// Some grouped exports are exercised only by their family's stories.
owners.add(umbrella);
} else {
throw new Error(
`No scoped Storybook route for ${owner} or ${umbrella}`,
);
}
}
for (const {packageName, directory, owner} of changedFolderMembers(
analysis.unresolvedComponentSources,
repoRoot,
)) {
routes ??= readPrA11yRoutes(repoRoot);
const umbrella = `${packageName}/${directory}`;
if (componentRoutesForFilters(routes, [owner]).length > 0) {
owners.add(owner);
} else if (componentRoutesForFilters(routes, [umbrella]).length > 0) {
// Some grouped exports are exercised only by their family's stories.
owners.add(umbrella);
} else {
throw new Error(`No scoped Storybook route for ${owner} or ${umbrella}`);
}
}
return [...owners].sort();
Expand Down
30 changes: 28 additions & 2 deletions .github/scripts/ci-heavy-lanes.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file Visual, accessibility and RTL run on main, not on pull request updates.
* @file Full visual, accessibility and RTL suites run on main; pull request
* updates check only touched components.
* @input CI, Main and Main heavy lanes workflows, and a mocked GitHub API
* @output Routing for the heavy owners, the required joins, and Main's
* release-check dispatch
Expand Down Expand Up @@ -32,7 +33,6 @@ const SUITE = {
'pr-a11y-specs': 'a11y',
'pr-a11y': 'a11y',
'visual-scout': 'visual',
'pr-visual': 'visual',
'pr-rtl-shard': 'rtl',
'pr-rtl-merge': 'rtl',
'pr-rtl': 'rtl',
Expand Down Expand Up @@ -63,6 +63,32 @@ describe('visual, accessibility and RTL owners', () => {
},
);

it('runs pr-visual on every pull request: its full check only with ci:run-visual, otherwise the default touched check', () => {
const visual = ci.jobs['pr-visual'];
const condition = ungated(visual.if);
expect(condition).toContain(
"(github.event_name == 'pull_request' &&\n needs.build-storybook.result == 'success')",
);
expect(visual.needs).toContain('run-labels');
const step = name =>
visual.steps.find(candidate => candidate.name === name);
expect(step('Run the visual gate for the touched components').if).toContain(
"needs.run-labels.outputs.visual == 'true'",
);
expect(step('Decide the default touched visual check').if).toContain(
"needs.run-labels.outputs.visual != 'true'",
);
const touched = step('Visual check for the default touched components');
expect(touched.if).toBe(
"steps.touched.outputs.has_components == 'true' && steps.touched.outputs.visual_components != ''",
);
expect(touched.run).toContain('gate.mjs check');
expect(touched.run).toContain('--tiers component');
expect(touched.run).toContain('--no-scout');
expect(touched.run).not.toContain('--dependents');
expect(touched.run).not.toContain('gate.mjs release');
});

it('decides the suites in one gate job from the trusted decision script', () => {
const gate = ci.jobs['run-labels'];
expect(gate['runs-on']).toBe('ubuntu-slim');
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/ci-test-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ describe('ci.yml RTL package sharding', () => {
'pr-rtl',
'pr-rtl-merge',
'pr-rtl-shard',
'pr-touched-components',
'pr-visual',
'visual-scout',
].sort(),
Expand Down
4 changes: 3 additions & 1 deletion .github/scripts/run-labels.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,10 @@ describe('run-labels decision', () => {
// Replay: each label's decision through every heavy owner's condition on a
// pull request whose other needs succeeded.
describe('each label runs only its own suite', () => {
// pr-visual runs on every pull request for its default touched check; its
// full check's step needs ci:run-visual (ci-heavy-lanes.test.mjs).
const OWNERS = {
visual: ['visual-scout', 'pr-visual'],
visual: ['visual-scout'],
a11y: ['pr-a11y-guards', 'pr-a11y-specs', 'pr-a11y'],
rtl: ['pr-rtl-shard', 'pr-rtl-merge', 'pr-rtl'],
};
Expand Down
Loading
Loading