Last active
August 17, 2021 18:13
-
-
Save wbern/d0ffb116804b2c4bb61da10249500163 to your computer and use it in GitHub Desktop.
Revisions
-
wbern revised this gist
Jun 1, 2021 . 3 changed files with 4 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -164,4 +164,4 @@ const exec = (forceChangedProjects?: string) => }); }); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -151,4 +151,4 @@ export function getCommand( (parsedForceChangedProjects.length > 0 ? " " + parsedForceChangedProjects.join(" ") : ""); return commandToRun; } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -50,6 +50,7 @@ export function getChangesFromCommit(commitToCompareFrom: string): string[] { "--no-pager", "diff", "--name-only", "--staged", "--no-renames", commitToCompareFrom, ]) @@ -113,4 +114,4 @@ export function parseBuildDirectionsString(buildDirectionsJson: string): string[ `Could not parse second argument "buildDirections"; needs to be an array like ['--to', '--from']` ); } } -
wbern revised this gist
May 18, 2021 . 2 changed files with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -164,4 +164,4 @@ const exec = (forceChangedProjects?: string) => }); }); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -103,7 +103,7 @@ export function getCommand( if (!mapFileToProject()) { if ( !isGlobalFileSafeFromSideEffects( [".dev-proxyrc.js", "README.MD", "jsconfig.json", ".vscode/launch.json", "chart"], file ) ) { @@ -151,4 +151,4 @@ export function getCommand( (parsedForceChangedProjects.length > 0 ? " " + parsedForceChangedProjects.join(" ") : ""); return commandToRun; } -
wbern revised this gist
May 18, 2021 . 1 changed file with 7 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -15,6 +15,12 @@ export function getRushConfig(): RushConfiguration { rushJson.rushJsonFolder = repoRoot; // eslint-disable-next-line @typescript-eslint/no-explicit-any rushJson.projects = rushJson.projects.map((project: any) => ({ ...project, projectFolder: path.resolve(repoRoot, project.projectFolder), })); return rushJson as RushConfiguration; } @@ -107,4 +113,4 @@ export function parseBuildDirectionsString(buildDirectionsJson: string): string[ `Could not parse second argument "buildDirections"; needs to be an array like ['--to', '--from']` ); } } -
wbern revised this gist
May 18, 2021 . 1 changed file with 10 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,12 +1,21 @@ import { spawnSync } from "child_process"; import process from "process"; import path from "path"; import fs from "fs"; import { RushConfiguration } from "@microsoft/rush-lib"; const debug = true; export function getRushConfig(): RushConfiguration { const repoRoot = path.join(__dirname, "../../../"); // eslint-disable-next-line @typescript-eslint/no-explicit-any let rushJson: any; eval(`rushJson = ${fs.readFileSync(path.join(repoRoot, "rush.json")).toString()}`); rushJson.rushJsonFolder = repoRoot; return rushJson as RushConfiguration; } export function printDebug(msg: string): void { -
wbern created this gist
May 18, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,21 @@ import { getCommand } from "./lib"; import { printCommand } from "./util"; import process from "process"; function main() { const [ // eslint-disable-next-line @typescript-eslint/no-unused-vars _, // eslint-disable-next-line @typescript-eslint/no-unused-vars __, command = "sleep", buildDirections = "['--to', '--from']", forceChangedProjects = "", ] = process.argv; const commandToExecute = getCommand(command, buildDirections, forceChangedProjects); printCommand(commandToExecute); } main(); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,3 @@ export interface FilesInProjectsOrGlobal { [key: string]: string[]; } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,167 @@ import * as util from "./util"; import * as lib from "./lib"; const actualUtil = jest.requireActual("./util"); const mockedUtil = util as jest.Mocked<typeof util>; const mockCommits = [ // latest commit "abcdef", "hijkmn", "opqrst", ]; jest.mock("./util"); const exec = (forceChangedProjects?: string) => lib.getCommand("sleep", "['--to', '--from']", forceChangedProjects); [true, false].forEach((CI) => { const getMockingMethodForChangedFiles = () => CI ? mockedUtil.getChangesFromCommit : mockedUtil.getStagedChanges; const commitToCompareFrom = CI ? mockCommits[Math.floor(Math.random() * mockCommits.length)] : mockCommits[0]; describe(`Given we are running the script on ${ CI ? "the jenkins pipeline" : "a local machine" }`, () => { beforeEach(() => { jest.clearAllMocks(); mockedUtil.parseBuildDirectionsString = actualUtil.parseBuildDirectionsString; mockedUtil.getCurrentCommit.mockImplementation(() => mockCommits[0]); // @ts-expect-error we dont want to stub the complete rush config mockedUtil.getRushConfig.mockImplementation(() => { return { rushJsonFolder: "/home/user/repo", projects: [ { packageName: "project-a", projectFolder: "/home/user/repo/apps/project-a" }, { packageName: "project-b", projectFolder: "/home/user/repo/apps/project-b" }, { packageName: "project-c", projectFolder: "/home/user/repo/apps/project-c" }, ], }; }); mockedUtil.isCI.mockReturnValue(CI); if (CI) { mockedUtil.getPreviousSuccessfulCommit.mockReturnValueOnce(commitToCompareFrom); } }); describe("and we have some changes inside a project", () => { it("should output the input command, plus found project.", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", ]); const expectedResult = "sleep --to project-a --from project-a"; expect(exec()).toBe(expectedResult); getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", "apps/project-a/src/util.js", "apps/project-a/package.json", ]); expect(exec()).toBe(expectedResult); }); describe("but the file is specified to be ignored", () => { it("should output a voiding echo command", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", ]); mockedUtil.isProjectFileToBeIgnoredForChangeDetection.mockImplementationOnce(() => true); expect(exec()).toBe( `echo "skipping sleep because no projects have changed from commit hash ${commitToCompareFrom}"` ); }); }); }); describe("and we have some changes inside multiple projects", () => { it("should output the input command, plus found project(s).", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", "apps/project-b/src/index.js", ]); const expectedResult = "sleep --to project-a --from project-a --to project-b --from project-b"; expect(exec()).toBe(expectedResult); getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", "apps/project-b/package.json", "apps/project-b/src/index.js", "apps/project-b/package.json", ]); expect(exec()).toBe(expectedResult); }); }); it("should output the input command, plus the forced projects", async () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => [ "apps/project-a/src/index.js", "apps/project-b/src/index.js", ]); const expectedResult = "sleep --to project-a --from project-a --to project-b --from project-b"; expect(exec("project-a")).toBe(expectedResult); }); describe("and we have modified a file outside the projects", () => { it("should build everything", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => ["package.json"]); expect(exec()).toBe("sleep"); }); describe("that global file is specified to be safe from side-effects to projects", () => { it("should output a voiding echo command", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => ["package.json"]); mockedUtil.isGlobalFileSafeFromSideEffects.mockImplementationOnce(() => true); expect(exec()).toBe( `echo "skipping sleep because no projects have changed from commit hash ${commitToCompareFrom}"` ); }); }); describe("but we have specified some project to be forced to be included", () => { it("should output the input command, plus the forced projects", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => ["package.json"]); expect(exec("project-a")).toBe("sleep"); }); }); }); describe("and we have not made any changes", () => { it("should output a voiding echo command", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => []); expect(exec()).toBe( `echo "skipping sleep because no projects have changed from commit hash ${commitToCompareFrom}"` ); }); describe("but we have specified some project to be forced to be included", () => { it("should output the input command, plus the forced projects", () => { getMockingMethodForChangedFiles().mockImplementationOnce(() => []); expect(exec("project-a")).toBe("sleep --to project-a --from project-a"); }); }); }); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,154 @@ import { RushConfigurationProject } from "@microsoft/rush-lib"; import path from "path"; import { printDebug, isCI, parseBuildDirectionsString, getRushConfig, getChangesFromCommit, getBranchNameInPipeline, getCurrentCommit, getStagedChanges, getMergeBaseCommit, getCustomCompareGitCommit, getPreviousSuccessfulCommit, isProjectFileToBeIgnoredForChangeDetection, isGlobalFileSafeFromSideEffects, } from "./util"; import { FilesInProjectsOrGlobal } from "./interfaces"; export function getCommand( command: string, buildDirectionsJson = "['--to', '--from']", forceChangedProjects = "" ): string { const rushJson = getRushConfig(); const buildDirections: string[] = parseBuildDirectionsString(buildDirectionsJson); let commitToCompareFrom; let files; if (isCI()) { const previousSuccessfulBuildCommit = getPreviousSuccessfulCommit(); if (getBranchNameInPipeline() === "master" && previousSuccessfulBuildCommit === null) { // master is brand new. This is rare/untested, but that likely means we'll want execute for every project. return command; } commitToCompareFrom = getCustomCompareGitCommit() || previousSuccessfulBuildCommit || getMergeBaseCommit(); if (commitToCompareFrom === null) { // might be first commit ever made? Execute everything return command; } printDebug( "[CI] comparing with commit hash " + (previousSuccessfulBuildCommit ? "(from a previous successful build) " : "(from last ancestor commit to master) ") + commitToCompareFrom ); files = getChangesFromCommit(commitToCompareFrom); } else { // executing on local machine commitToCompareFrom = getCurrentCommit(); if (commitToCompareFrom === null) { // might be first commit ever made? Execute everything return command; } printDebug("[Local] comparing local staged files with latest commit " + commitToCompareFrom); files = getStagedChanges(commitToCompareFrom); } const categorizedFiles: FilesInProjectsOrGlobal = { "[global]": [], "[global]-ignored": [], }; const changedProjects = files.reduce((projects: Record<string, string[]>, file) => { const resolvedFile = path.join(rushJson.rushJsonFolder, file); const mapFileToProject = () => rushJson.projects.some((project: RushConfigurationProject) => { if (resolvedFile.startsWith(project.projectFolder)) { categorizedFiles[project.packageName] = categorizedFiles[project.packageName] || []; if (isProjectFileToBeIgnoredForChangeDetection([], file)) { categorizedFiles[`${project.packageName}-ignored`] = categorizedFiles[`${project.packageName}-ignored`] || []; categorizedFiles[`${project.packageName}-ignored`].push(file); return true; } else { projects[project.packageName] = projects[project.packageName] || []; categorizedFiles[project.packageName].push(file); projects[project.packageName].push(file); return true; } } return false; }); if (!mapFileToProject()) { if ( !isGlobalFileSafeFromSideEffects( [".dev-proxyrc.js", "README.MD", "jsconfig.json", ".vscode", "chart"], file ) ) { categorizedFiles["[global]"].push(file); } else { categorizedFiles["[global]-ignored"].push(file); } } return projects; }, {}); printDebug( "changes:\n" + Object.keys(categorizedFiles) .filter((key) => categorizedFiles[key].length) .map((key) => [`------------------\nScope: ${key}\n`, ...categorizedFiles[key]]) .flatMap((item) => item) .join("\n") + "\n\n" ); if (categorizedFiles["[global]"].length > 0) { return command; } if (Object.keys(changedProjects).length === 0 && forceChangedProjects === "") { return `echo "skipping ${command} because no projects have changed from commit hash ${commitToCompareFrom}"`; } const parsedForceChangedProjects = forceChangedProjects .split(" ") .filter( (projectName) => projectName !== "" && !Object.keys(changedProjects).includes(projectName) ) .map((projectName) => buildDirections.flatMap((d) => [d, projectName]).join(" ")); const commandToRun = [ command, ...Object.keys(changedProjects).flatMap((projectName) => { return buildDirections.flatMap((d) => [d, projectName]); }), ].join(" ") + (parsedForceChangedProjects.length > 0 ? " " + parsedForceChangedProjects.join(" ") : ""); return commandToRun; } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,101 @@ import { spawnSync } from "child_process"; import process from "process"; import { RushConfiguration } from "@microsoft/rush-lib"; const debug = true; export function getRushConfig(): RushConfiguration { return RushConfiguration.loadFromDefaultLocation(); } export function printDebug(msg: string): void { if (debug) { process.stderr.write(msg); } } export function isProjectFileToBeIgnoredForChangeDetection( ignoreArray: string[], file: string ): boolean { return ignoreArray.includes(file); } export function isGlobalFileSafeFromSideEffects(ignoreArray: string[], file: string): boolean { return ignoreArray.includes(file); } export function getMergeBaseCommit(): string { return spawnSync("git", ["merge-base", "HEAD", "origin/master"]).stdout.toString().trim(); } export function getChangesFromCommit(commitToCompareFrom: string): string[] { return spawnSync("git", [ "--no-pager", "diff", "--name-only", "--no-renames", commitToCompareFrom, ]) .stdout.toString() .trim() .split("\n") .filter((item) => item !== ""); } export function printCommand(command: string): void { // output will be consumed as a command in Jenkinsfile // eslint-disable-next-line console.log(command); } export function getCurrentCommit(): string | null { try { return spawnSync("git", ["rev-parse", "HEAD"]).stdout.toString().trim(); } catch (e) { return null; } } export function getStagedChanges(commitToCompareFrom: string): string[] { return spawnSync("git", ["--no-pager", "diff", "--name-only", "--staged", commitToCompareFrom]) .stdout.toString() .trim() .split("\n") .filter((item) => item !== ""); } export function getCustomCompareGitCommit(): string | null { return (process.env && process.env.CUSTOM_COMPARE_GIT_COMMIT) || null; } export function getPreviousSuccessfulCommit(): string | null { if ( process.env && typeof process.env.GIT_PREVIOUS_NON_FAILED_COMMIT === "string" && /[0-9a-f]{7,40}/.test(process.env.GIT_PREVIOUS_NON_FAILED_COMMIT || "") ) { return process.env.GIT_PREVIOUS_NON_FAILED_COMMIT; } return null; } export function getBranchNameInPipeline(): string | null { return process.env?.BRANCH_NAME || null; } export function isCI(): boolean { return !!getBranchNameInPipeline(); } export function parseBuildDirectionsString(buildDirectionsJson: string): string[] { try { return JSON.parse(buildDirectionsJson.replace(/['`]/g, '"')); } catch (e) { throw new Error( `Could not parse second argument "buildDirections"; needs to be an array like ['--to', '--from']` ); } }