Skip to content

Commit 8405f6c

Browse files
authored
Yarn Workspaces Follow Up (#481)
* Fix topo sort trying to read from packages that aren't within the repository * Fix incorrect documentation url references * Fix coverage also including test utility files * Fix devserver vite config not being linted * Fix incorrect building commands for devserver * Fix the artifact command * Update workflows to not execute jobs at all if there were no detected changes for the corresponding package * Update matrix jobs to properly not execute when they don't have to * Make devserver and docserver still execute even if the tabs or libraries job doesn't execute * Make end github job execute properly * Remove trailing spaces * Test including lockfile information for hasChanges * Linting and lodash import path fixes * Fix incorrect package name processing * Fix broken package names being used in commands * Add tests for lockfile functions * Minor documentation fixes * Add tests to command options * FIx missing icons * Fix typo * Fix lint * Avoid soft breaks * Fix lint
1 parent 5ce950d commit 8405f6c

File tree

36 files changed

+1456
-187
lines changed

36 files changed

+1456
-187
lines changed

.github/actions/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"@actions/artifact": "^2.3.2",
1414
"@actions/core": "^1.11.1",
1515
"@actions/exec": "^1.1.1",
16-
"lodash": "^4.17.21"
16+
"lodash": "^4.17.21",
17+
"snyk-nodejs-lockfile-parser": "^2.4.2"
1718
},
1819
"scripts": {
1920
"build": "node ./build.js",

.github/actions/src/__tests__/commons.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ vi.mock(import('lodash/memoize.js'), () => ({
88

99
const mockedExecOutput = vi.spyOn(exec, 'getExecOutput');
1010

11-
describe(commons.checkForChanges, () => {
11+
describe(commons.checkDirForChanges, () => {
1212
function mockChanges(value: boolean) {
1313
mockedExecOutput.mockResolvedValueOnce({
1414
exitCode: value ? 1 : 0, stdout: '', stderr: ''
@@ -17,14 +17,14 @@ describe(commons.checkForChanges, () => {
1717

1818
it('should return true if git diff exits with non zero code', async () => {
1919
mockChanges(true);
20-
await expect(commons.checkForChanges('/')).resolves.toEqual(true);
20+
await expect(commons.checkDirForChanges('/')).resolves.toEqual(true);
2121
expect(mockedExecOutput).toHaveBeenCalledOnce();
2222
});
2323

2424
it('should return false if git diff exits with 0', async () => {
2525
mockChanges(false);
2626

27-
await expect(commons.checkForChanges('/')).resolves.toEqual(false);
27+
await expect(commons.checkDirForChanges('/')).resolves.toEqual(false);
2828
expect(mockedExecOutput).toHaveBeenCalledOnce();
2929
});
3030
});

.github/actions/src/commons.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,12 @@ export function isPackageRecord(obj: unknown): obj is PackageRecord {
6060
return true;
6161
}
6262

63-
// Not using the repotools version since this uses @action/exec instead of
64-
// calling execFile from child_process
65-
export async function getGitRoot() {
66-
const { stdout } = await getExecOutput('git rev-parse --show-toplevel');
67-
return stdout.trim();
68-
}
69-
7063
/**
7164
* Returns `true` if there are changes present in the given directory relative to
7265
* the master branch\
7366
* Used to determine, particularly for libraries, if running tests and tsc are necessary
7467
*/
75-
export const checkForChanges = memoize(async (directory: string) => {
68+
export const checkDirForChanges = memoize(async (directory: string) => {
7669
const { exitCode } = await getExecOutput(
7770
'git',
7871
['--no-pager', 'diff', '--quiet', 'origin/master', '--', directory],

.github/actions/src/gitRoot.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getExecOutput } from '@actions/exec';
2+
3+
// Not using the repotools version since this uses @action/exec instead of
4+
// calling execFile from child_process
5+
async function getGitRoot() {
6+
const { stdout } = await getExecOutput('git rev-parse --show-toplevel');
7+
return stdout.trim();
8+
}
9+
10+
/**
11+
* Path to the root of the git repository
12+
*/
13+
export const gitRoot = await getGitRoot();

.github/actions/src/info/__tests__/index.test.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { Dirent } from 'fs';
22
import fs from 'fs/promises';
33
import pathlib from 'path';
4+
import * as core from '@actions/core';
45
import { describe, expect, test, vi } from 'vitest';
56
import * as git from '../../commons.js';
6-
import { getAllPackages, getRawPackages } from '../index.js';
7+
import * as lockfiles from '../../lockfiles/index.js';
8+
import { getAllPackages, getRawPackages, main } from '../index.js';
79

8-
const mockedCheckChanges = vi.spyOn(git, 'checkForChanges');
10+
const mockedCheckChanges = vi.spyOn(git, 'checkDirForChanges');
911

1012
vi.mock(import('path'), async importOriginal => {
1113
const { posix } = await importOriginal();
@@ -16,6 +18,10 @@ vi.mock(import('path'), async importOriginal => {
1618
};
1719
});
1820

21+
vi.mock(import('../../gitRoot.js'), () => ({
22+
gitRoot: 'root'
23+
}));
24+
1925
class NodeError extends Error {
2026
constructor(public readonly code: string) {
2127
super();
@@ -39,7 +45,7 @@ const mockDirectory: Record<string, string | Record<string, unknown>> = {
3945
'package.json': JSON.stringify({
4046
name: '@sourceacademy/bundle-bundle0',
4147
devDependencies: {
42-
'@sourceacademy/modules-lib': 'workspace:^'
48+
'@sourceacademy/modules-lib': 'workspace:^',
4349
}
4450
})
4551
},
@@ -49,7 +55,8 @@ const mockDirectory: Record<string, string | Record<string, unknown>> = {
4955
'package.json': JSON.stringify({
5056
name: '@sourceacademy/tab-Tab0',
5157
dependencies: {
52-
'@sourceacademy/bundle-bundle0': 'workspace:^'
58+
lodash: '^4.1.1',
59+
'@sourceacademy/bundle-bundle0': 'workspace:^',
5360
},
5461
devDependencies: {
5562
playwright: '^1.54.0'
@@ -114,6 +121,7 @@ function mockReadFile(path: string) {
114121

115122
vi.spyOn(fs, 'readdir').mockImplementation(mockReaddir as any);
116123
vi.spyOn(fs, 'readFile').mockImplementation(mockReadFile as any);
124+
vi.spyOn(lockfiles, 'hasLockFileChanged').mockResolvedValue(false);
117125

118126
describe(getRawPackages, () => {
119127
test('maxDepth = 1', async () => {
@@ -125,7 +133,7 @@ describe(getRawPackages, () => {
125133
const [[name, packageData]] = results;
126134
expect(name).toEqual('@sourceacademy/modules');
127135
expect(packageData.hasChanges).toEqual(true);
128-
expect(git.checkForChanges).toHaveBeenCalledOnce();
136+
expect(git.checkDirForChanges).toHaveBeenCalledOnce();
129137
});
130138

131139
test('maxDepth = 3', async () => {
@@ -211,3 +219,33 @@ describe(getAllPackages, () => {
211219
expect(results['@sourceacademy/modules-lib'].changes).toEqual(true);
212220
});
213221
});
222+
223+
describe(main, () => {
224+
const mockedSetOutput = vi.spyOn(core, 'setOutput');
225+
226+
vi.spyOn(core.summary, 'addHeading').mockImplementation(() => core.summary);
227+
vi.spyOn(core.summary, 'addTable').mockImplementation(() => core.summary);
228+
vi.spyOn(core.summary, 'write').mockImplementation(() => Promise.resolve(core.summary));
229+
230+
test('Does not write packages with no changes to the output', async () => {
231+
mockedCheckChanges.mockImplementation(path => {
232+
return Promise.resolve(path === 'root/src/tabs/tab0');
233+
});
234+
235+
await main();
236+
const { mock: { calls } } = mockedSetOutput;
237+
238+
expect(mockedSetOutput).toHaveBeenCalledTimes(6);
239+
240+
expect(calls[0]).toEqual(['bundles', []]);
241+
expect(calls[1]).toEqual(['tabs', [expect.objectContaining({ changes: true })]]);
242+
expect(calls[2]).toEqual(['libs', []]);
243+
244+
// These next two are undefined because the mock implementations
245+
// don't return any info about them
246+
expect(calls[3]).toEqual(['devserver', undefined]);
247+
expect(calls[4]).toEqual(['docserver', undefined]);
248+
249+
expect(calls[5]).toEqual(['workflows', false]);
250+
});
251+
});

.github/actions/src/info/action.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ description: Gets which packages are currently present in the repository
33

44
outputs:
55
libs:
6-
description: The list of library packages present
6+
description: The list of library packages that have changes
77
bundles:
8-
description: The list of bundles packages present
8+
description: The list of bundles packages that have changes
99
tabs:
10-
description: The list of tabs packages present
10+
description: The list of tabs packages that have changes
1111
devserver:
1212
description: Information for the devserver
1313
docserver:

.github/actions/src/info/index.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import utils from 'util';
44
import * as core from '@actions/core';
55
import type { SummaryTableRow } from '@actions/core/lib/summary.js';
66
import packageJson from '../../../../package.json' with { type: 'json' };
7-
import { checkForChanges, getGitRoot, type PackageRecord, type RawPackageRecord } from '../commons.js';
7+
import { checkDirForChanges, type PackageRecord, type RawPackageRecord } from '../commons.js';
8+
import { gitRoot } from '../gitRoot.js';
9+
import { getPackagesWithResolutionChanges, hasLockFileChanged } from '../lockfiles/index.js';
810
import { topoSortPackages } from './sorter.js';
911

1012
const packageNameRE = /^@sourceacademy\/(.+?)-(.+)$/u;
@@ -14,23 +16,34 @@ const packageNameRE = /^@sourceacademy\/(.+?)-(.+)$/u;
1416
* an unprocessed format
1517
*/
1618
export async function getRawPackages(gitRoot: string, maxDepth?: number) {
19+
let packagesWithResolutionChanges: Set<string> | null = null;
20+
21+
// If there are lock file changes we need to set hasChanges to true for
22+
// that package even if that package's directory has no changes
23+
if (await hasLockFileChanged()) {
24+
packagesWithResolutionChanges = await getPackagesWithResolutionChanges();
25+
}
26+
1727
const output: Record<string, RawPackageRecord> = {};
1828

29+
/**
30+
* Search the given directory for package.json files
31+
*/
1932
async function recurser(currentDir: string, currentDepth: number) {
2033
const items = await fs.readdir(currentDir, { withFileTypes: true });
2134
await Promise.all(items.map(async item => {
2235
if (item.isFile()) {
2336
if (item.name === 'package.json') {
2437
try {
2538
const [hasChanges, packageJson] = await Promise.all([
26-
checkForChanges(currentDir),
39+
checkDirForChanges(currentDir),
2740
fs.readFile(pathlib.join(currentDir, 'package.json'), 'utf-8')
2841
.then(JSON.parse)
2942
]);
3043

3144
output[packageJson.name] = {
3245
directory: currentDir,
33-
hasChanges,
46+
hasChanges: packagesWithResolutionChanges?.has(packageJson.name) ?? hasChanges,
3447
package: packageJson
3548
};
3649
} catch (error) {
@@ -93,16 +106,18 @@ export function processRawPackages(topoOrder: string[], packages: Record<string,
93106
if (!packageInfo.hasChanges) {
94107
if (packageInfo.package.dependencies) {
95108
for (const name of Object.keys(packageInfo.package.dependencies)) {
96-
if (packages[name].hasChanges) {
109+
if (packages[name]?.hasChanges) {
97110
packageInfo.hasChanges = true;
98111
break;
99112
}
100113
}
101114
}
102115

116+
// If hasChanges still hasn't been set yet, we can proceed to iterate
117+
// through devDependencies as well
103118
if (!packageInfo.hasChanges && packageInfo.package.devDependencies) {
104119
for (const name of Object.keys(packageInfo.package.devDependencies)) {
105-
if (packages[name].hasChanges) {
120+
if (packages[name]?.hasChanges) {
106121
packageInfo.hasChanges = true;
107122
break;
108123
}
@@ -203,15 +218,14 @@ function setOutputs(
203218
devserver: PackageRecord,
204219
docserver: PackageRecord
205220
) {
206-
core.setOutput('bundles', bundles);
207-
core.setOutput('tabs', tabs);
208-
core.setOutput('libs', libs);
221+
core.setOutput('bundles', bundles.filter(x => x.changes));
222+
core.setOutput('tabs', tabs.filter(x => x.changes));
223+
core.setOutput('libs', libs.filter(x => x.changes));
209224
core.setOutput('devserver', devserver);
210225
core.setOutput('docserver', docserver);
211226
}
212227

213-
async function main() {
214-
const gitRoot = await getGitRoot();
228+
export async function main() {
215229
const { packages, bundles, tabs, libs } = await getAllPackages(gitRoot);
216230

217231
const { repository } = packageJson;
@@ -260,7 +274,7 @@ async function main() {
260274
packages['@sourceacademy/modules-docserver']
261275
);
262276

263-
const workflows = await checkForChanges(pathlib.join(gitRoot, '.github/workflows'));
277+
const workflows = await checkDirForChanges(pathlib.join(gitRoot, '.github/workflows'));
264278
core.setOutput('workflows', workflows);
265279
}
266280

@@ -269,6 +283,6 @@ if (process.env.GITHUB_ACTIONS) {
269283
try {
270284
await main();
271285
} catch (error: any) {
272-
core.setFailed(error.message);
286+
core.setFailed(error);
273287
}
274288
}

.github/actions/src/load-artifacts/__tests__/artifact.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ArtifactNotFoundError } from '@actions/artifact';
12
import * as core from '@actions/core';
23
import * as exec from '@actions/exec';
34
import * as manifest from '@sourceacademy/modules-repotools/manifest';
@@ -10,10 +11,10 @@ vi.mock(import('@actions/core'), async importOriginal => {
1011
...original,
1112
// Mock these functions to remove stdout output
1213
error: vi.fn(),
13-
info: () => {},
14-
startGroup: () => {},
14+
info: () => { },
15+
startGroup: () => { },
1516
setFailed: vi.fn(),
16-
endGroup: () => {}
17+
endGroup: () => { }
1718
};
1819
});
1920
const mockedResolveAllTabs = vi.spyOn(manifest, 'resolveAllTabs');
@@ -41,6 +42,7 @@ test('tab resolution errors cause setFailed to be called', async () => {
4142

4243
await main();
4344

45+
expect(mockedResolveAllTabs).toHaveBeenCalledOnce();
4446
expect(core.error).toHaveBeenCalledExactlyOnceWith('error1');
4547
expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('Tab resolution failed with errors');
4648
});
@@ -68,22 +70,26 @@ test('tabs that can\'t be found are built', async () => {
6870
if (name === 'Tab0-tab') {
6971
return { artifact: { id: 0 } };
7072
}
71-
throw new Error();
73+
throw new ArtifactNotFoundError();
7274
});
7375

7476
await main();
7577

7678
expect(mockedGetArtifact).toHaveBeenCalledTimes(2);
79+
expect(mockedResolveAllTabs).toHaveBeenCalledOnce();
80+
7781
const [[artifactCall0], [artifactCall1]] = mockedGetArtifact.mock.calls;
7882
expect(artifactCall0).toEqual('Tab0-tab');
7983
expect(artifactCall1).toEqual('Tab1-tab');
8084

8185
expect(exec.exec).toHaveBeenCalledTimes(2);
82-
const [[,execCall0], [,execCall1]] = vi.mocked(exec.exec).mock.calls;
83-
console.log('args are', execCall0);
86+
const [[execCmd0, execCall0], [execCmd1, execCall1]] = vi.mocked(exec.exec).mock.calls;
87+
88+
expect(execCmd0).toEqual('yarn workspaces focus');
8489
expect(execCall0).toContain('@sourceacademy/tab-Tab1');
8590
expect(execCall0).not.toContain('@sourceacademy/tab-Tab0');
8691

92+
expect(execCmd1).toEqual('yarn workspaces foreach -pA');
8793
expect(execCall1).toContain('@sourceacademy/tab-Tab1');
8894
expect(execCall1).not.toContain('@sourceacademy/tab-Tab0');
8995
});
@@ -101,11 +107,12 @@ test('install failure means build doesn\'t happen', async () => {
101107
}
102108
});
103109

104-
mockedGetArtifact.mockRejectedValueOnce(new Error());
105-
110+
mockedGetArtifact.mockRejectedValueOnce(new ArtifactNotFoundError());
106111
mockedExec.mockResolvedValueOnce(1);
112+
107113
await main();
108114

115+
expect(mockedGetArtifact).toHaveBeenCalledOnce();
109116
expect(exec.exec).toHaveBeenCalledOnce();
110-
expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('yarn workspace focus failed for Tab0');
117+
expect(core.setFailed).toHaveBeenCalledExactlyOnceWith('yarn workspace focus failed');
111118
});

0 commit comments

Comments
 (0)