mirror of
https://github.com/tiktok/sparo.git
synced 2024-11-27 09:36:04 -05:00
feat: build test for a real repo
This commit is contained in:
parent
7cf0c1443f
commit
ab7cefefca
27 changed files with 892 additions and 182 deletions
|
@ -57,7 +57,9 @@ an empty directory.`);
|
|||
terminal.writeDebugLine('full clone start...');
|
||||
const cloneArgs: string[] = ['clone', repository, directory];
|
||||
const result: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: cloneArgs
|
||||
args: cloneArgs,
|
||||
// This is clone command, no need to find git folder
|
||||
workingDirectory: process.cwd()
|
||||
});
|
||||
if (result?.status) {
|
||||
throw new Error(`git clone failed with exit code ${result.status}`);
|
||||
|
@ -79,7 +81,9 @@ an empty directory.`);
|
|||
directory
|
||||
];
|
||||
const result: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: cloneArgs
|
||||
args: cloneArgs,
|
||||
// This is clone command, no need to find git folder
|
||||
workingDirectory: process.cwd()
|
||||
});
|
||||
if (result?.status) {
|
||||
throw new Error(`git clone failed with exit code ${result.status}`);
|
||||
|
@ -101,7 +105,9 @@ an empty directory.`);
|
|||
directory
|
||||
];
|
||||
const result: child_process.SpawnSyncReturns<string> = this._gitService.executeGitCommand({
|
||||
args: cloneArgs
|
||||
args: cloneArgs,
|
||||
// This is clone command, no need to find git folder
|
||||
workingDirectory: process.cwd()
|
||||
});
|
||||
if (result?.status) {
|
||||
throw new Error(`git clone failed with exit code ${result.status}`);
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@rushstack/node-core-library": "~3.64.2",
|
||||
"sparo": "workspace:*",
|
||||
"jest-diff": "~29.7.0"
|
||||
"build-test-utilities": "workspace:*",
|
||||
"sparo": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/heft": "0.64.3",
|
||||
|
|
|
@ -1,24 +1,10 @@
|
|||
import * as path from 'path';
|
||||
import { Async, Executable, FileSystem, type FolderItem, Text } from '@rushstack/node-core-library';
|
||||
import { diff } from 'jest-diff';
|
||||
import type { SpawnSyncReturns } from 'child_process';
|
||||
import {
|
||||
ICommandDefinition,
|
||||
executeCommandsAndCollectOutputs,
|
||||
updateOrCompareOutputs
|
||||
} from 'build-test-utilities';
|
||||
import type { IRunScriptOptions } from '@rushstack/heft';
|
||||
|
||||
interface IScenarioDefinition {
|
||||
/**
|
||||
* The scenario name. It is used to generate the output file name.
|
||||
*
|
||||
* For example, if the name is "top-level-help", the output file name will be "top-level-help.txt".
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The command line arguments to run. This doesn't include the command name itself.
|
||||
*
|
||||
* For example, if the command is "sparo clone --help", the args will be ["clone", "--help"].
|
||||
*/
|
||||
args: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This build test is highly inspired by the build test for api-extractor in rushstack.
|
||||
*/
|
||||
|
@ -30,184 +16,44 @@ export async function runAsync(runScriptOptions: IRunScriptOptions): Promise<voi
|
|||
},
|
||||
heftConfiguration: { buildFolderPath }
|
||||
} = runScriptOptions;
|
||||
const binPath: string = path.join(buildFolderPath, 'node_modules', '.bin', 'sparo');
|
||||
const tempFolder: string = path.join(buildFolderPath, 'temp', 'etc');
|
||||
|
||||
const scenarios: IScenarioDefinition[] = [
|
||||
const commandDefinitions: ICommandDefinition[] = [
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'top-level-help',
|
||||
args: ['--help']
|
||||
},
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'clone-help',
|
||||
args: ['clone', '--help']
|
||||
},
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'checkout-help',
|
||||
args: ['checkout', '--help']
|
||||
},
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'top-level-nonexistent-command',
|
||||
args: ['nonexistent-command']
|
||||
}
|
||||
// FIXME: This is currently broken -- it simply ignores the unrecognized parameter
|
||||
// {
|
||||
// kind: 'sparo-command',
|
||||
// name: 'checkout-nonexistent-parameter',
|
||||
// args: ['checkout', '--nonexistent-parameter']
|
||||
// }
|
||||
];
|
||||
|
||||
/**
|
||||
* Run each scenario and generate outputs
|
||||
*/
|
||||
await FileSystem.ensureEmptyFolderAsync(tempFolder);
|
||||
for (const scenario of scenarios) {
|
||||
const { name, args } = scenario;
|
||||
const result: SpawnSyncReturns<string> = Executable.spawnSync(binPath, args, {
|
||||
environment: {
|
||||
...process.env,
|
||||
// Always use color for the output
|
||||
FORCE_COLOR: 'true'
|
||||
}
|
||||
});
|
||||
await executeCommandsAndCollectOutputs({
|
||||
buildFolderPath,
|
||||
commandDefinitions
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to run "sparo ${args.join(' ')}" with exit code ${result.status}\n${result.stderr}`
|
||||
);
|
||||
}
|
||||
|
||||
const outputPath: string = path.join(tempFolder, `${name}.txt`);
|
||||
FileSystem.writeFile(
|
||||
outputPath,
|
||||
`Running "sparo ${args.join(' ')}":\n${processVersionString(result.stdout)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Files under outFolderPath are tracked by Git, files under inFolderPath are temporary files. During a local build,
|
||||
* --production is false, temporary files are copied to outFolderPath. During a CI build, --production is true, the
|
||||
* files with same name under these two folders are compared and CI build fails if they are different.
|
||||
*
|
||||
* This ensures that the temporary files must be up to date in the PR, and people who review the PR must approve any
|
||||
* changes.
|
||||
*/
|
||||
const inFolderPath: string = tempFolder;
|
||||
const outFolderPath: string = `${buildFolderPath}/etc`;
|
||||
await FileSystem.ensureFolderAsync(outFolderPath);
|
||||
|
||||
const inFolderPaths: AsyncIterable<string> = enumerateFolderPaths(inFolderPath, '');
|
||||
const outFolderPaths: AsyncIterable<string> = enumerateFolderPaths(outFolderPath, '');
|
||||
const outFolderPathsSet: Set<string> = new Set<string>();
|
||||
|
||||
for await (const outFolderPath of outFolderPaths) {
|
||||
outFolderPathsSet.add(outFolderPath);
|
||||
}
|
||||
|
||||
const nonMatchingFiles: string[] = [];
|
||||
const nonMatchingFileErrorMessages: Map<string, string> = new Map<string, string>();
|
||||
await Async.forEachAsync(
|
||||
inFolderPaths,
|
||||
async (folderItemPath: string) => {
|
||||
outFolderPathsSet.delete(folderItemPath);
|
||||
|
||||
const sourceFileContents: string = await FileSystem.readFileAsync(inFolderPath + folderItemPath);
|
||||
const outFilePath: string = outFolderPath + folderItemPath;
|
||||
|
||||
let outFileContents: string | undefined;
|
||||
try {
|
||||
outFileContents = await FileSystem.readFileAsync(outFilePath);
|
||||
} catch (e) {
|
||||
if (!FileSystem.isNotExistError(e)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedSourceFileContents: string = Text.convertToLf(sourceFileContents);
|
||||
const normalizedOutFileContents: string | undefined = outFileContents
|
||||
? Text.convertToLf(outFileContents)
|
||||
: undefined;
|
||||
|
||||
if (normalizedSourceFileContents !== normalizedOutFileContents) {
|
||||
nonMatchingFiles.push(outFilePath);
|
||||
if (production) {
|
||||
// Display diff only when running in production mode, mostly for CI build
|
||||
nonMatchingFileErrorMessages.set(
|
||||
outFilePath,
|
||||
diff(normalizedOutFileContents, normalizedSourceFileContents) || ''
|
||||
);
|
||||
}
|
||||
if (!production) {
|
||||
await FileSystem.writeFileAsync(outFilePath, normalizedSourceFileContents, {
|
||||
ensureFolderExists: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 10
|
||||
}
|
||||
);
|
||||
|
||||
if (outFolderPathsSet.size > 0) {
|
||||
nonMatchingFiles.push(...outFolderPathsSet);
|
||||
if (!production) {
|
||||
await Async.forEachAsync(
|
||||
outFolderPathsSet,
|
||||
async (outFolderPath) => {
|
||||
await FileSystem.deleteFileAsync(`${outFolderPath}/${outFolderPath}`);
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nonMatchingFiles.length > 0) {
|
||||
const errorLines: string[] = [];
|
||||
for (const nonMatchingFile of nonMatchingFiles.sort()) {
|
||||
errorLines.push(` ${nonMatchingFile}`);
|
||||
const errorMessage: string | undefined = nonMatchingFileErrorMessages.get(nonMatchingFile);
|
||||
if (errorMessage) {
|
||||
errorLines.push(`${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (production) {
|
||||
logger.emitError(
|
||||
new Error(
|
||||
'The following file(s) do not match the expected output. Build this project in non-production ' +
|
||||
`mode and commit the changes:\n${errorLines.join('\n')}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logger.emitWarning(
|
||||
new Error(
|
||||
`The following file(s) do not match the expected output and must be committed to Git:\n` +
|
||||
errorLines.join('\n')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all x.y.z version strings with __VERSION__.
|
||||
*/
|
||||
function processVersionString(text: string): string {
|
||||
return text.replace(/\d+\.\d+\.\d+/g, '__VERSION__');
|
||||
}
|
||||
|
||||
async function* enumerateFolderPaths(
|
||||
absoluteFolderPath: string,
|
||||
relativeFolderPath: string
|
||||
): AsyncIterable<string> {
|
||||
const folderItems: FolderItem[] = await FileSystem.readFolderItemsAsync(absoluteFolderPath);
|
||||
for (const folderItem of folderItems) {
|
||||
const childRelativeFolderPath: string = `${relativeFolderPath}/${folderItem.name}`;
|
||||
if (folderItem.isDirectory()) {
|
||||
yield* enumerateFolderPaths(`${absoluteFolderPath}/${folderItem.name}`, childRelativeFolderPath);
|
||||
} else {
|
||||
yield childRelativeFolderPath;
|
||||
}
|
||||
}
|
||||
await updateOrCompareOutputs({
|
||||
buildFolderPath,
|
||||
logger,
|
||||
production
|
||||
});
|
||||
}
|
||||
|
|
19
build-tests/sparo-real-repo-test/.eslintrc.js
Normal file
19
build-tests/sparo-real-repo-test/.eslintrc.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
// This is a workaround for https://github.com/eslint/eslint/issues/3458
|
||||
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution');
|
||||
// This is a workaround for https://github.com/microsoft/rushstack/issues/3021
|
||||
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names');
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@rushstack/heft-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool',
|
||||
'@rushstack/heft-node-rig/profiles/default/includes/eslint/mixins/friendly-locals'
|
||||
],
|
||||
parserOptions: { tsconfigRootDir: __dirname },
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {}
|
||||
}
|
||||
]
|
||||
};
|
21
build-tests/sparo-real-repo-test/LICENSE
Normal file
21
build-tests/sparo-real-repo-test/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) TikTok Pte. Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
build-tests/sparo-real-repo-test/README.md
Normal file
11
build-tests/sparo-real-repo-test/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# sparo-output-test
|
||||
|
||||
Building this project tests sparo command outputs
|
||||
|
||||
# Details
|
||||
|
||||
`lib/start-test.js` is run after building the project. This scripts generate the output text files under `temp/etc`. In local builds, those files are copied to `etc` folder. During a CI build, the files under these two folders are compared and the CI build fails if they are different. This ensures that files under `etc` folder must be up to date in the PR, and people who review the PR must approve any changes.
|
||||
|
||||
# How to fix the build errors
|
||||
|
||||
Run `rush build -t sparo-output-test` to regenerate files under `etc` folder and commit them into Git.
|
24
build-tests/sparo-real-repo-test/config/heft.json
Normal file
24
build-tests/sparo-real-repo-test/config/heft.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
|
||||
|
||||
"extends": "@rushstack/heft-node-rig/profiles/default/config/heft.json",
|
||||
|
||||
"phasesByName": {
|
||||
"build": {
|
||||
"tasksByName": {
|
||||
"post-compile": {
|
||||
"taskDependencies": ["typescript"],
|
||||
|
||||
"taskPlugin": {
|
||||
"pluginName": "run-script-plugin",
|
||||
"pluginPackage": "@rushstack/heft",
|
||||
|
||||
"options": {
|
||||
"scriptPath": "lib/start-test.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
build-tests/sparo-real-repo-test/config/rig.json
Normal file
5
build-tests/sparo-real-repo-test/config/rig.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
|
||||
"rigPackageName": "@rushstack/heft-node-rig"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
Running "sparo checkout --profile my-team":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
|
||||
[90m--[[39m [1mgit checkout[22m [90m]-------------------------------------------------------------[39m
|
||||
Your branch is up to date with 'origin/main'.
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Syncing checkout with the Sparo profile: my-team
|
||||
|
||||
Checking out and updating core files...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out skeleton...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Sparse checkout completed in __DURATION__
|
29
build-tests/sparo-real-repo-test/etc/checkout-profile.txt
Normal file
29
build-tests/sparo-real-repo-test/etc/checkout-profile.txt
Normal file
|
@ -0,0 +1,29 @@
|
|||
Running "sparo checkout --profile my-team":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
|
||||
[90m--[[39m [1mgit checkout[22m [90m]-------------------------------------------------------------[39m
|
||||
Your branch is up to date with 'origin/main'.
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Syncing checkout with the Sparo profile: my-team
|
||||
|
||||
Checking out and updating core files...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out skeleton...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out __FOLDER_COUNT__ folders...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Sparse checkout completed in __DURATION__
|
40
build-tests/sparo-real-repo-test/etc/clone.txt
Normal file
40
build-tests/sparo-real-repo-test/etc/clone.txt
Normal file
|
@ -0,0 +1,40 @@
|
|||
Running "sparo clone git@github.com:Azure/azure-sdk-for-js.git":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
Initializing working directory...
|
||||
|
||||
[90m--[[39m [1mgit clone[22m [90m]----------------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out and updating core files...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out skeleton...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Applying recommended configuration...
|
||||
|
||||
[90m--[[39m [1mgit maintenance[22m [90m]----------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
[32mSuccess: Working directory "azure-sdk-for-js" was prepared in __DURATION__.[39m
|
||||
|
||||
Don't forget to change your shell path:
|
||||
[36mcd azure-sdk-for-js[39m
|
||||
|
||||
Your next step is to choose a Sparo profile for checkout.
|
||||
To see available profiles in this repo:
|
||||
[36msparo list-profiles[39m
|
||||
To checkout and set profile:
|
||||
[36msparo checkout --profile <profile_name>[39m
|
||||
To checkout and add profile:
|
||||
[36msparo checkout --add-profile <profile_name>[39m
|
||||
To create a new profile:
|
||||
[36msparo init-profile --profile <profile_name>[39m
|
11
build-tests/sparo-real-repo-test/etc/init-profile.txt
Normal file
11
build-tests/sparo-real-repo-test/etc/init-profile.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
Running "sparo init-profile --profile my-team":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
[32mSuccessfully initialized the "my-team" profile.[39m
|
||||
|
||||
Next step: Open this file in your editor and configure the project selectors:
|
||||
|
||||
[36m__WORKING_DIRECTORY__/common/sparo-profiles/my-team.json[39m
|
|
@ -0,0 +1,20 @@
|
|||
Running "sparo list-profiles --project @azure/core-auth":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
Listing profiles...
|
||||
|
||||
Checking out and updating core files...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out skeleton...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
@azure/core-auth was included in the below profiles:
|
||||
my-team
|
20
build-tests/sparo-real-repo-test/etc/list-profiles.txt
Normal file
20
build-tests/sparo-real-repo-test/etc/list-profiles.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
Running "sparo list-profiles":
|
||||
|
||||
[1mSparo accelerator for Git __VERSION__ -[22m[36m https://tiktok.github.io/sparo/[39m
|
||||
Node.js version is __VERSION__ (LTS)
|
||||
Git version is __VERSION__
|
||||
|
||||
Listing profiles...
|
||||
|
||||
Checking out and updating core files...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
Checking out skeleton...
|
||||
|
||||
[90m--[[39m [1mgit sparse-checkout[22m [90m]------------------------------------------------------[39m
|
||||
[90m-------------------------------------------------------------------------------[39m
|
||||
|
||||
All available profiles:
|
||||
my-team
|
25
build-tests/sparo-real-repo-test/package.json
Normal file
25
build-tests/sparo-real-repo-test/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "sparo-real-repo-test",
|
||||
"description": "Building this project tests sparo in a real repository",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"_phase:build": "heft run --only build -- --clean",
|
||||
"build": "heft build --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rushstack/node-core-library": "~3.64.2",
|
||||
"build-test-utilities": "workspace:*",
|
||||
"sparo": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/heft": "0.64.3",
|
||||
"@rushstack/heft-node-rig": "2.4.5",
|
||||
"@types/heft-jest": "1.0.6",
|
||||
"@types/node": "20.11.16",
|
||||
"eslint": "8.56.0",
|
||||
"typescript": "~5.3.3"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT"
|
||||
}
|
113
build-tests/sparo-real-repo-test/src/start-test.ts
Normal file
113
build-tests/sparo-real-repo-test/src/start-test.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import * as path from 'path';
|
||||
import { FileSystem } from '@rushstack/node-core-library';
|
||||
import type { IRunScriptOptions } from '@rushstack/heft';
|
||||
import {
|
||||
ICommandDefinition,
|
||||
executeCommandsAndCollectOutputs,
|
||||
updateOrCompareOutputs
|
||||
} from 'build-test-utilities';
|
||||
|
||||
/**
|
||||
* This build test is highly inspired by the build test for api-extractor in rushstack.
|
||||
*/
|
||||
export async function runAsync(runScriptOptions: IRunScriptOptions): Promise<void> {
|
||||
const {
|
||||
heftTaskSession: {
|
||||
logger,
|
||||
parameters: { production }
|
||||
},
|
||||
heftConfiguration: { buildFolderPath }
|
||||
} = runScriptOptions;
|
||||
|
||||
const temporaryDirectory: string = path.resolve(buildFolderPath, 'temp');
|
||||
const testRepoURL: string = 'git@github.com:Azure/azure-sdk-for-js.git';
|
||||
const repoFolder: string = path.resolve(temporaryDirectory, 'azure-sdk-for-js');
|
||||
|
||||
await FileSystem.deleteFolderAsync(repoFolder);
|
||||
|
||||
const commandDefinitions: ICommandDefinition[] = [
|
||||
// sparo clone git@github.com:Azure/azure-sdk-for-js.git
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'clone',
|
||||
args: ['clone', testRepoURL],
|
||||
currentWorkingDirectory: temporaryDirectory
|
||||
},
|
||||
// sparo init-profile --profile my-team
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'init-profile',
|
||||
args: ['init-profile', '--profile', 'my-team'],
|
||||
currentWorkingDirectory: repoFolder
|
||||
},
|
||||
// sparo checkout --profile my-team - extra step to checkout an empty profile
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'checkout-empty-profile',
|
||||
args: ['checkout', '--profile', 'my-team'],
|
||||
currentWorkingDirectory: repoFolder
|
||||
},
|
||||
// Prepare my-team profile
|
||||
{
|
||||
kind: 'custom-callback',
|
||||
name: 'prepare-my-team-profile',
|
||||
callback: async () => {
|
||||
await FileSystem.writeFileAsync(
|
||||
path.resolve(repoFolder, 'common/sparo-profiles/my-team.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
$schema: 'https://tiktok.github.io/sparo/schemas/sparo-profile.schema.json',
|
||||
selections: [
|
||||
{
|
||||
selector: '--to',
|
||||
argument: '@azure/arm-commerce'
|
||||
}
|
||||
],
|
||||
includeFolders: [],
|
||||
excludeFolders: []
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
// sparo checkout --profile my-team
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'checkout-profile',
|
||||
args: ['checkout', '--profile', 'my-team'],
|
||||
currentWorkingDirectory: repoFolder
|
||||
},
|
||||
// sparo list-profiles
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'list-profiles',
|
||||
args: ['list-profiles'],
|
||||
currentWorkingDirectory: repoFolder
|
||||
},
|
||||
// sparo list-profiles --project @azure/core-auth
|
||||
{
|
||||
kind: 'sparo-command',
|
||||
name: 'list-profiles-with-project',
|
||||
args: ['list-profiles', '--project', '@azure/core-auth'],
|
||||
currentWorkingDirectory: repoFolder
|
||||
}
|
||||
];
|
||||
|
||||
await executeCommandsAndCollectOutputs({
|
||||
buildFolderPath,
|
||||
commandDefinitions
|
||||
});
|
||||
|
||||
await updateOrCompareOutputs({
|
||||
buildFolderPath,
|
||||
logger,
|
||||
production
|
||||
});
|
||||
|
||||
// Clean up the temporary directory in CI builds, but leave it for local debugging
|
||||
if (production) {
|
||||
await FileSystem.deleteFolderAsync(repoFolder);
|
||||
}
|
||||
}
|
8
build-tests/sparo-real-repo-test/tsconfig.json
Normal file
8
build-tests/sparo-real-repo-test/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
|
||||
"extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "heft-jest"]
|
||||
}
|
||||
}
|
19
build-tests/test-utilities/.eslintrc.js
Normal file
19
build-tests/test-utilities/.eslintrc.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
// This is a workaround for https://github.com/eslint/eslint/issues/3458
|
||||
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution');
|
||||
// This is a workaround for https://github.com/microsoft/rushstack/issues/3021
|
||||
require('@rushstack/heft-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names');
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@rushstack/heft-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool',
|
||||
'@rushstack/heft-node-rig/profiles/default/includes/eslint/mixins/friendly-locals'
|
||||
],
|
||||
parserOptions: { tsconfigRootDir: __dirname },
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {}
|
||||
}
|
||||
]
|
||||
};
|
21
build-tests/test-utilities/LICENSE
Normal file
21
build-tests/test-utilities/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) TikTok Pte. Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
build-tests/test-utilities/README.md
Normal file
11
build-tests/test-utilities/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# sparo-output-test
|
||||
|
||||
Building this project tests sparo command outputs
|
||||
|
||||
# Details
|
||||
|
||||
`lib/start-test.js` is run after building the project. This scripts generate the output text files under `temp/etc`. In local builds, those files are copied to `etc` folder. During a CI build, the files under these two folders are compared and the CI build fails if they are different. This ensures that files under `etc` folder must be up to date in the PR, and people who review the PR must approve any changes.
|
||||
|
||||
# How to fix the build errors
|
||||
|
||||
Run `rush build -t sparo-output-test` to regenerate files under `etc` folder and commit them into Git.
|
5
build-tests/test-utilities/config/rig.json
Normal file
5
build-tests/test-utilities/config/rig.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
|
||||
"rigPackageName": "@rushstack/heft-node-rig"
|
||||
}
|
26
build-tests/test-utilities/package.json
Normal file
26
build-tests/test-utilities/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "build-test-utilities",
|
||||
"description": "Utilities to do build test",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"_phase:build": "heft run --only build -- --clean",
|
||||
"build": "heft build --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rushstack/node-core-library": "~3.64.2",
|
||||
"jest-diff": "~29.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/heft": "0.64.3",
|
||||
"@rushstack/heft-node-rig": "2.4.5",
|
||||
"@types/heft-jest": "1.0.6",
|
||||
"@types/node": "20.11.16",
|
||||
"eslint": "8.56.0",
|
||||
"typescript": "~5.3.3"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT"
|
||||
}
|
281
build-tests/test-utilities/src/index.ts
Normal file
281
build-tests/test-utilities/src/index.ts
Normal file
|
@ -0,0 +1,281 @@
|
|||
import * as path from 'path';
|
||||
import { Async, Executable, FileSystem, Text, type FolderItem } from '@rushstack/node-core-library';
|
||||
import { diff } from 'jest-diff';
|
||||
import type { SpawnSyncReturns } from 'child_process';
|
||||
import type { IScopedLogger } from '@rushstack/heft';
|
||||
|
||||
export type ICommandDefinition = ISparoCommandDefinition | ICustomCallbackDefinition;
|
||||
|
||||
export interface ISparoCommandDefinition {
|
||||
kind: 'sparo-command';
|
||||
/**
|
||||
* The scenario name. It is used to generate the output file name.
|
||||
*
|
||||
* For example, if the name is "top-level-help", the output file name will be "top-level-help.txt".
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The command line arguments to run. This doesn't include the command name itself.
|
||||
*
|
||||
* For example, if the command is "sparo clone --help", the args will be ["clone", "--help"].
|
||||
*/
|
||||
args: string[];
|
||||
|
||||
/**
|
||||
* The working directory
|
||||
*/
|
||||
currentWorkingDirectory?: string;
|
||||
}
|
||||
|
||||
export interface ICustomCallbackDefinition {
|
||||
kind: 'custom-callback';
|
||||
/**
|
||||
* Name of the custom definition
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Callback function to run logic between commands
|
||||
*/
|
||||
callback: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IExecuteCommandsAndCollectOutputsOptions {
|
||||
commandDefinitions: ICommandDefinition[];
|
||||
buildFolderPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a list of predefined commands, collecting the sparo outputs to a temporary folder.
|
||||
*/
|
||||
export async function executeCommandsAndCollectOutputs({
|
||||
commandDefinitions,
|
||||
buildFolderPath
|
||||
}: IExecuteCommandsAndCollectOutputsOptions): Promise<void> {
|
||||
const sparoBinPath: string = path.join(buildFolderPath, 'node_modules', '.bin', 'sparo');
|
||||
const tempFolder: string = path.join(buildFolderPath, 'temp', 'etc');
|
||||
|
||||
/**
|
||||
* Run each scenario and generate outputs
|
||||
*/
|
||||
await FileSystem.ensureEmptyFolderAsync(tempFolder);
|
||||
for (const commandListDefinition of commandDefinitions) {
|
||||
const { kind } = commandListDefinition;
|
||||
switch (commandListDefinition.kind) {
|
||||
case 'sparo-command': {
|
||||
const { name, args, currentWorkingDirectory } = commandListDefinition;
|
||||
const result: SpawnSyncReturns<string> = Executable.spawnSync(sparoBinPath, args, {
|
||||
currentWorkingDirectory,
|
||||
environment: {
|
||||
...process.env,
|
||||
// Always use color for the output
|
||||
FORCE_COLOR: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to run "sparo ${args.join(' ')}" with exit code ${result.status}\n${result.stderr}`
|
||||
);
|
||||
}
|
||||
|
||||
const outputPath: string = path.join(tempFolder, `${name}.txt`);
|
||||
FileSystem.writeFile(
|
||||
outputPath,
|
||||
`Running "sparo ${args.join(' ')}":\n${processSparoOutput(
|
||||
result.stdout,
|
||||
currentWorkingDirectory || process.cwd()
|
||||
)}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'custom-callback': {
|
||||
const { name, callback } = commandListDefinition;
|
||||
try {
|
||||
await callback();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to run custom callback function for ${name}:\n${e.message}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unrecognized command kind: ${kind}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IUpdateOrCompareOutputs {
|
||||
buildFolderPath: string;
|
||||
production: boolean;
|
||||
logger: IScopedLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on buildFolderPath, we have a inFolderPath and outFolderPath.
|
||||
*
|
||||
* Files under outFolderPath are tracked by Git, files under inFolderPath are temporary files. During a local build,
|
||||
* --production is false, temporary files are copied to outFolderPath. During a CI build, --production is true, the
|
||||
* files with same name under these two folders are compared and CI build fails if they are different.
|
||||
*
|
||||
* The file structure could be:
|
||||
*
|
||||
* -- buildFolder
|
||||
* |- temp
|
||||
* |- etc
|
||||
* |- foo.txt
|
||||
* |- etc
|
||||
* |- foo.txt
|
||||
*
|
||||
* This ensures that the temporary files must be up to date in the PR, and people who review the PR must approve any
|
||||
* changes.
|
||||
*/
|
||||
export async function updateOrCompareOutputs({
|
||||
buildFolderPath,
|
||||
production,
|
||||
logger
|
||||
}: IUpdateOrCompareOutputs): Promise<void> {
|
||||
const inFolderPath: string = `${buildFolderPath}/temp/etc`;
|
||||
const outFolderPath: string = `${buildFolderPath}/etc`;
|
||||
await FileSystem.ensureFolderAsync(outFolderPath);
|
||||
|
||||
const inFolderPaths: AsyncIterable<string> = enumerateFolderPaths(inFolderPath, '');
|
||||
const outFolderPaths: AsyncIterable<string> = enumerateFolderPaths(outFolderPath, '');
|
||||
const outFolderPathsSet: Set<string> = new Set<string>();
|
||||
|
||||
for await (const outFolderPath of outFolderPaths) {
|
||||
outFolderPathsSet.add(outFolderPath);
|
||||
}
|
||||
|
||||
const nonMatchingFiles: string[] = [];
|
||||
const nonMatchingFileErrorMessages: Map<string, string> = new Map<string, string>();
|
||||
await Async.forEachAsync(
|
||||
inFolderPaths,
|
||||
async (folderItemPath: string) => {
|
||||
outFolderPathsSet.delete(folderItemPath);
|
||||
|
||||
const sourceFileContents: string = await FileSystem.readFileAsync(inFolderPath + folderItemPath);
|
||||
const outFilePath: string = outFolderPath + folderItemPath;
|
||||
|
||||
let outFileContents: string | undefined;
|
||||
try {
|
||||
outFileContents = await FileSystem.readFileAsync(outFilePath);
|
||||
} catch (e) {
|
||||
if (!FileSystem.isNotExistError(e)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedSourceFileContents: string = Text.convertToLf(sourceFileContents);
|
||||
const normalizedOutFileContents: string | undefined = outFileContents
|
||||
? Text.convertToLf(outFileContents)
|
||||
: undefined;
|
||||
|
||||
if (normalizedSourceFileContents !== normalizedOutFileContents) {
|
||||
nonMatchingFiles.push(outFilePath);
|
||||
if (production) {
|
||||
// Display diff only when running in production mode, mostly for CI build
|
||||
nonMatchingFileErrorMessages.set(
|
||||
outFilePath,
|
||||
diff(normalizedOutFileContents, normalizedSourceFileContents) || ''
|
||||
);
|
||||
}
|
||||
if (!production) {
|
||||
await FileSystem.writeFileAsync(outFilePath, normalizedSourceFileContents, {
|
||||
ensureFolderExists: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 10
|
||||
}
|
||||
);
|
||||
|
||||
if (outFolderPathsSet.size > 0) {
|
||||
nonMatchingFiles.push(...outFolderPathsSet);
|
||||
if (!production) {
|
||||
await Async.forEachAsync(
|
||||
outFolderPathsSet,
|
||||
async (outFolderPath) => {
|
||||
await FileSystem.deleteFileAsync(`${outFolderPath}/${outFolderPath}`);
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nonMatchingFiles.length > 0) {
|
||||
const errorLines: string[] = [];
|
||||
for (const nonMatchingFile of nonMatchingFiles.sort()) {
|
||||
errorLines.push(` ${nonMatchingFile}`);
|
||||
const errorMessage: string | undefined = nonMatchingFileErrorMessages.get(nonMatchingFile);
|
||||
if (errorMessage) {
|
||||
errorLines.push(`${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (production) {
|
||||
logger.emitError(
|
||||
new Error(
|
||||
'The following file(s) do not match the expected output. Build this project in non-production ' +
|
||||
`mode and commit the changes:\n${errorLines.join('\n')}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logger.emitWarning(
|
||||
new Error(
|
||||
`The following file(s) do not match the expected output and must be committed to Git:\n` +
|
||||
errorLines.join('\n')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processSparoOutput(text: string, workingDirectory: string): string {
|
||||
return [
|
||||
replaceVersionString,
|
||||
replaceDurationString,
|
||||
replaceWorkingDirectoryPath,
|
||||
replaceFolderCountString
|
||||
].reduce((text, fn) => fn(text, workingDirectory), text);
|
||||
}
|
||||
/**
|
||||
* Replace all x.y.z version strings with __VERSION__.
|
||||
*/
|
||||
function replaceVersionString(text: string): string {
|
||||
return text.replace(/\d+\.\d+\.\d+/g, '__VERSION__');
|
||||
}
|
||||
/**
|
||||
* Replace all "in xx.yy seconds" strings with "in __DURATION__ seconds".
|
||||
*/
|
||||
function replaceDurationString(text: string): string {
|
||||
return text.replace(/in \d+(\.\d+)? seconds/g, 'in __DURATION__');
|
||||
}
|
||||
/**
|
||||
* Replace all "<workingDirectory>" strings with "__WORKING_DIRECTORY__".
|
||||
*/
|
||||
function replaceWorkingDirectoryPath(text: string, workingDirectory: string): string {
|
||||
return text.replace(new RegExp(workingDirectory, 'g'), '__WORKING_DIRECTORY__');
|
||||
}
|
||||
/**
|
||||
* Replace "Checking out x folders" with "Checking out __FOLDER_COUNT__ folders".
|
||||
*/
|
||||
function replaceFolderCountString(text: string): string {
|
||||
return text.replace(/Checking out \d+ folders/g, 'Checking out __FOLDER_COUNT__ folders');
|
||||
}
|
||||
|
||||
async function* enumerateFolderPaths(
|
||||
absoluteFolderPath: string,
|
||||
relativeFolderPath: string
|
||||
): AsyncIterable<string> {
|
||||
const folderItems: FolderItem[] = await FileSystem.readFolderItemsAsync(absoluteFolderPath);
|
||||
for (const folderItem of folderItems) {
|
||||
const childRelativeFolderPath: string = `${relativeFolderPath}/${folderItem.name}`;
|
||||
if (folderItem.isDirectory()) {
|
||||
yield* enumerateFolderPaths(`${absoluteFolderPath}/${folderItem.name}`, childRelativeFolderPath);
|
||||
} else {
|
||||
yield childRelativeFolderPath;
|
||||
}
|
||||
}
|
||||
}
|
8
build-tests/test-utilities/tsconfig.json
Normal file
8
build-tests/test-utilities/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
|
||||
"extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "heft-jest"]
|
||||
}
|
||||
}
|
|
@ -154,9 +154,9 @@ importers:
|
|||
'@rushstack/node-core-library':
|
||||
specifier: ~3.64.2
|
||||
version: 3.64.2(@types/node@20.11.16)
|
||||
jest-diff:
|
||||
specifier: ~29.7.0
|
||||
version: 29.7.0
|
||||
build-test-utilities:
|
||||
specifier: workspace:*
|
||||
version: link:../test-utilities
|
||||
sparo:
|
||||
specifier: workspace:*
|
||||
version: link:../../apps/sparo
|
||||
|
@ -180,6 +180,65 @@ importers:
|
|||
specifier: ~5.3.3
|
||||
version: 5.3.3
|
||||
|
||||
../../build-tests/sparo-real-repo-test:
|
||||
dependencies:
|
||||
'@rushstack/node-core-library':
|
||||
specifier: ~3.64.2
|
||||
version: 3.64.2(@types/node@20.11.16)
|
||||
build-test-utilities:
|
||||
specifier: workspace:*
|
||||
version: link:../test-utilities
|
||||
sparo:
|
||||
specifier: workspace:*
|
||||
version: link:../../apps/sparo
|
||||
devDependencies:
|
||||
'@rushstack/heft':
|
||||
specifier: 0.64.3
|
||||
version: 0.64.3(@types/node@20.11.16)
|
||||
'@rushstack/heft-node-rig':
|
||||
specifier: 2.4.5
|
||||
version: 2.4.5(@rushstack/heft@0.64.3)(@types/node@20.11.16)
|
||||
'@types/heft-jest':
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6
|
||||
'@types/node':
|
||||
specifier: 20.11.16
|
||||
version: 20.11.16
|
||||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
typescript:
|
||||
specifier: ~5.3.3
|
||||
version: 5.3.3
|
||||
|
||||
../../build-tests/test-utilities:
|
||||
dependencies:
|
||||
'@rushstack/node-core-library':
|
||||
specifier: ~3.64.2
|
||||
version: 3.64.2(@types/node@20.11.16)
|
||||
jest-diff:
|
||||
specifier: ~29.7.0
|
||||
version: 29.7.0
|
||||
devDependencies:
|
||||
'@rushstack/heft':
|
||||
specifier: 0.64.3
|
||||
version: 0.64.3(@types/node@20.11.16)
|
||||
'@rushstack/heft-node-rig':
|
||||
specifier: 2.4.5
|
||||
version: 2.4.5(@rushstack/heft@0.64.3)(@types/node@20.11.16)
|
||||
'@types/heft-jest':
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6
|
||||
'@types/node':
|
||||
specifier: 20.11.16
|
||||
version: 20.11.16
|
||||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
typescript:
|
||||
specifier: ~5.3.3
|
||||
version: 5.3.3
|
||||
|
||||
packages:
|
||||
|
||||
/@aashutoshrathi/word-wrap@1.2.6:
|
||||
|
|
50
common/sparo-profiles/my-team.json
Normal file
50
common/sparo-profiles/my-team.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* OWNER: <your team name>
|
||||
* PURPOSE: <what you use this profile for>
|
||||
*/
|
||||
{
|
||||
"$schema": "https://tiktok.github.io/sparo/schemas/sparo-profile.schema.json",
|
||||
|
||||
/**
|
||||
* A list of Rush project selectors indicating the project folders to be
|
||||
* included for sparse checkout. The selectors will be combined to make
|
||||
* the union superset of projects. See the Rush selector docs for details:
|
||||
* https://rushjs.io/pages/developer/selecting_subsets/
|
||||
*/
|
||||
"selections": [
|
||||
/**
|
||||
* For example, include all Rush projects tagged with "tag:my-team"
|
||||
* as well as the dependency workspace projects needed to build them.
|
||||
*/
|
||||
// {
|
||||
// "selector": "--to",
|
||||
// "argument": "tag:my-team"
|
||||
// },
|
||||
/**
|
||||
* For example, include the project called "my-library", as well as all
|
||||
* projects that are impacted by changes to it, as well as the dependency
|
||||
* projects needed to build everything.
|
||||
*/
|
||||
// {
|
||||
// "selector": "--from",
|
||||
// "argument": "my-library"
|
||||
// }
|
||||
],
|
||||
|
||||
/**
|
||||
* A list of arbitrary additional folders to be included for checkout,
|
||||
* not necessarily corresponding to any workspace project.
|
||||
*/
|
||||
"includeFolders": [
|
||||
// "path/to/include"
|
||||
],
|
||||
|
||||
/**
|
||||
* A list of folders to be excluded from the checkout. This field takes precedence
|
||||
* over the "includeFolders" and "selections" fields, guaranteeing that the
|
||||
* specified path will definitely not be included.
|
||||
*/
|
||||
"excludeFolders": [
|
||||
// "path/to/exclude"
|
||||
]
|
||||
}
|
|
@ -440,6 +440,14 @@
|
|||
"packageName": "sparo-output-test",
|
||||
"projectFolder": "build-tests/sparo-output-test"
|
||||
},
|
||||
{
|
||||
"packageName": "sparo-real-repo-test",
|
||||
"projectFolder": "build-tests/sparo-real-repo-test"
|
||||
},
|
||||
{
|
||||
"packageName": "build-test-utilities",
|
||||
"projectFolder": "build-tests/test-utilities"
|
||||
},
|
||||
|
||||
// Sparo
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue