diff --git a/.changeset/eighty-schools-admire.md b/.changeset/eighty-schools-admire.md new file mode 100644 index 000000000..058f712c9 --- /dev/null +++ b/.changeset/eighty-schools-admire.md @@ -0,0 +1,5 @@ +--- +"@changesets/config": minor +--- + +Support and validation for the new `changedFilePatterns` option has been added. diff --git a/.changeset/healthy-rats-chew.md b/.changeset/healthy-rats-chew.md new file mode 100644 index 000000000..bbbb954ec --- /dev/null +++ b/.changeset/healthy-rats-chew.md @@ -0,0 +1,5 @@ +--- +"@changesets/git": minor +--- + +`getChangedPackagesSinceRef` accepts now a new `changedFilePatterns` option. It can be used to determine which packages should be classified as changed. You can pass an array of glob patterns to it. diff --git a/.changeset/spicy-apes-visit.md b/.changeset/spicy-apes-visit.md new file mode 100644 index 000000000..bc545b15e --- /dev/null +++ b/.changeset/spicy-apes-visit.md @@ -0,0 +1,5 @@ +--- +"@changesets/types": patch +--- + +Added `changedFilePatterns` property to `Config` and `WrittenConfig` types. diff --git a/.changeset/wicked-years-share.md b/.changeset/wicked-years-share.md new file mode 100644 index 000000000..3fc69dfee --- /dev/null +++ b/.changeset/wicked-years-share.md @@ -0,0 +1,14 @@ +--- +"@changesets/cli": minor +--- + +A new config-level `changedFilePatterns` option has been added. You can configure it with an array of glob patterns like here: + +```json +// .changeset/config.json +{ + "changedFilePatterns": ["src/**"] +} +``` + +Files that do not match the configured pattern won't contribute to the "changed" status of the package to which they belong. This both affects `changesets add` and `changeset status`. diff --git a/packages/apply-release-plan/src/index.test.ts b/packages/apply-release-plan/src/index.test.ts index 118562fcf..8d07a81b4 100644 --- a/packages/apply-release-plan/src/index.test.ts +++ b/packages/apply-release-plan/src/index.test.ts @@ -47,6 +47,7 @@ class FakeReleasePlan { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], @@ -89,6 +90,7 @@ async function testSetup( fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], @@ -661,6 +663,7 @@ describe("apply release plan", () => { linked: [], access: "restricted", baseBranch: "main", + changedFilePatterns: ["**"], updateInternalDependencies: "patch", privatePackages: { version: true, tag: false }, ignore: [], @@ -736,6 +739,7 @@ describe("apply release plan", () => { linked: [], access: "restricted", baseBranch: "main", + changedFilePatterns: ["**"], updateInternalDependencies: "patch", privatePackages: { version: true, tag: false }, ignore: [], @@ -992,6 +996,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1107,6 +1112,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1207,6 +1213,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1306,6 +1313,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1408,6 +1416,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1523,6 +1532,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1631,6 +1641,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1730,6 +1741,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies, ignore: [], @@ -1830,6 +1842,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], @@ -2058,6 +2071,7 @@ describe("apply release plan", () => { linked: [], access: "restricted", baseBranch: "main", + changedFilePatterns: ["**"], changelog: [ path.resolve(__dirname, "test-utils/simple-get-changelog-entry"), null, @@ -2198,6 +2212,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "patch", ignore: [], @@ -2306,6 +2321,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], @@ -2426,6 +2442,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], @@ -2560,6 +2577,7 @@ describe("apply release plan", () => { fixed: [], linked: [], access: "restricted", + changedFilePatterns: ["**"], baseBranch: "main", updateInternalDependencies: "minor", ignore: [], diff --git a/packages/cli/src/commands/add/index.ts b/packages/cli/src/commands/add/index.ts index 2112c82c3..680629257 100644 --- a/packages/cli/src/commands/add/index.ts +++ b/packages/cli/src/commands/add/index.ts @@ -42,6 +42,7 @@ export default async function add( const changedPackages = await git.getChangedPackagesSinceRef({ cwd, ref: config.baseBranch, + changedFilePatterns: config.changedFilePatterns, }); const changedPackagesName = changedPackages .filter((pkg) => isListablePackage(config, pkg.packageJson)) diff --git a/packages/cli/src/commands/status/__tests__/status.ts b/packages/cli/src/commands/status/__tests__/status.ts index dd332fe22..c7770b7c3 100644 --- a/packages/cli/src/commands/status/__tests__/status.ts +++ b/packages/cli/src/commands/status/__tests__/status.ts @@ -1,16 +1,39 @@ -import fs from "fs-extra"; -import path from "path"; -import * as git from "@changesets/git"; import { defaultConfig } from "@changesets/config"; -import { silenceLogsInBlock, testdir } from "@changesets/test-utils"; +import * as git from "@changesets/git"; +import { gitdir, silenceLogsInBlock } from "@changesets/test-utils"; +import { ReleasePlan } from "@changesets/types"; import writeChangeset from "@changesets/write"; - +import fs from "fs-extra"; +import path from "path"; +import spawn from "spawndamnit"; import status from ".."; -import humanId from "human-id"; +function replaceHumanIds(releaseObj: ReleasePlan | undefined) { + if (!releaseObj) { + return; + } + let counter = 0; + const changesetNames = new Map(); -jest.mock("human-id"); -jest.mock("@changesets/git"); + return { + ...releaseObj, + changesets: releaseObj.changesets.map((changeset) => { + if (changesetNames.get(changeset.id)) { + throw new Error("Duplicate changeset id found: " + changeset.id); + } + const replacedId = `~changeset-${++counter}~`; + changesetNames.set(changeset.id, replacedId); + return { + ...changeset, + id: replacedId, + }; + }), + releases: releaseObj.releases.map((release) => ({ + ...release, + changesets: release.changesets.map((id) => changesetNames.get(id) || id), + })), + }; +} describe("status", () => { silenceLogsInBlock(); @@ -20,7 +43,7 @@ describe("status", () => { }); it("should get the status for a simple changeset and return the release object", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -32,17 +55,12 @@ describe("status", () => { ".changeset/config.json": JSON.stringify({}), }); - const changesetID = "ascii"; - // @ts-ignore - humanId.mockReturnValueOnce(changesetID); - // @ts-ignore - git.getChangedPackagesSinceRef.mockImplementation(() => [ - { - packageJson: { name: "pkg-a", version: "1.0.0" }, - dir: "/fake/folder/doesnt/matter", - }, - ]); + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); await writeChangeset( { summary: "This is a summary", @@ -50,12 +68,15 @@ describe("status", () => { }, cwd ); - const releaseObj = await status(cwd, {}, defaultConfig); - expect(releaseObj).toMatchInlineSnapshot(` + await git.add(".", cwd); + await git.commit("updated a", cwd); + + const releaseObj = await status(cwd, { since: "main" }, defaultConfig); + expect(replaceHumanIds(releaseObj)).toMatchInlineSnapshot(` { "changesets": [ { - "id": "ascii", + "id": "~changeset-1~", "releases": [ { "name": "pkg-a", @@ -69,7 +90,7 @@ describe("status", () => { "releases": [ { "changesets": [ - "ascii", + "~changeset-1~", ], "name": "pkg-a", "newVersion": "1.1.0", @@ -82,7 +103,7 @@ describe("status", () => { }); it("should exit early with a non-zero error code when there are changed packages but no changesets", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -96,21 +117,24 @@ describe("status", () => { // @ts-ignore jest.spyOn(process, "exit").mockImplementation(() => {}); - // @ts-ignore - git.getChangedPackagesSinceRef.mockImplementation(() => [ - { - packageJson: { name: "pkg-a", version: "1.0.0" }, - dir: "/fake/folder/doesnt/matter", - }, - ]); - await status(cwd, {}, defaultConfig); + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); + + await git.add(".", cwd); + await git.commit("updated a", cwd); + + await status(cwd, { since: "main" }, defaultConfig); expect(process.exit).toHaveBeenCalledWith(1); }); it("should not exit early with a non-zero error code when there are no changed packages", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -124,10 +148,10 @@ describe("status", () => { // @ts-ignore jest.spyOn(process, "exit").mockImplementation(() => {}); - // @ts-ignore - git.getChangedPackagesSinceRef.mockImplementation(() => []); - const releaseObj = await status(cwd, {}, defaultConfig); + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + const releaseObj = await status(cwd, { since: "main" }, defaultConfig); expect(process.exit).not.toHaveBeenCalled(); expect(releaseObj).toEqual({ @@ -138,7 +162,7 @@ describe("status", () => { }); it("should not exit early with a non-zero code when there are changed packages and also a changeset", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -152,17 +176,13 @@ describe("status", () => { // @ts-ignore jest.spyOn(process, "exit").mockImplementation(() => {}); - // @ts-ignore - git.getChangedPackagesSinceRef.mockImplementation(() => [ - { - packageJson: { name: "pkg-a", version: "1.0.0" }, - dir: "/fake/folder/doesnt/matter", - }, - ]); - const changesetID = "ascii"; - // @ts-ignore - humanId.mockReturnValueOnce(changesetID); + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); await writeChangeset( { summary: "This is a summary", @@ -170,14 +190,19 @@ describe("status", () => { }, cwd ); - await status(cwd, {}, defaultConfig); + + await git.add(".", cwd); + await git.commit("updated a", cwd); + + await status(cwd, { since: "main" }, defaultConfig); expect(process.exit).not.toHaveBeenCalled(); }); it.skip("should respect the verbose flag", () => false); + it("should respect the output flag", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -189,18 +214,12 @@ describe("status", () => { ".changeset/config.json": JSON.stringify({}), }); - const output = "nonsense.json"; + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); - // @ts-ignore - git.getChangedPackagesSinceRef.mockImplementation(() => [ - { - packageJson: { name: "pkg-a", version: "1.0.0" }, - dir: "/fake/folder/doesnt/matter", - }, - ]); - const changesetID = "ascii"; - // @ts-ignore - humanId.mockReturnValueOnce(changesetID); + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); await writeChangeset( { @@ -209,16 +228,26 @@ describe("status", () => { }, cwd ); - const probsUndefined = await status(cwd, { output }, defaultConfig); + + await git.add(".", cwd); + await git.commit("updated a", cwd); + + const output = "nonsense.json"; + + const probsUndefined = await status( + cwd, + { since: "main", output }, + defaultConfig + ); const releaseObj = await fs.readFile(path.join(cwd, output), "utf-8"); expect(probsUndefined).toEqual(undefined); - expect(JSON.parse(releaseObj)).toMatchInlineSnapshot(` + expect(replaceHumanIds(JSON.parse(releaseObj))).toMatchInlineSnapshot(` { "changesets": [ { - "id": "ascii", + "id": "~changeset-1~", "releases": [ { "name": "pkg-a", @@ -231,7 +260,148 @@ describe("status", () => { "releases": [ { "changesets": [ - "ascii", + "~changeset-1~", + ], + "name": "pkg-a", + "newVersion": "1.1.0", + "oldVersion": "1.0.0", + "type": "minor", + }, + ], + } + `); + }); + + it("should not exit early with a non-zero error code when there are no changed packages matching the pattern", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + ".changeset/config.json": JSON.stringify({}), + }); + + // @ts-ignore + jest.spyOn(process, "exit").mockImplementation(() => {}); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + await fs.outputFile( + path.join(cwd, "packages/pkg-a/unrelated.json"), + JSON.stringify({}) + ); + + await git.add(".", cwd); + await git.commit("add unrelated thing", cwd); + + const releaseObj = await status( + cwd, + { since: "main" }, + { ...defaultConfig, changedFilePatterns: ["src/**"] } + ); + + expect(process.exit).not.toHaveBeenCalled(); + expect(releaseObj).toEqual({ + changesets: [], + releases: [], + preState: undefined, + }); + }); + + it("should exit early with a non-zero error code when there are changed packages matching the pattern but no changesets", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + "packages/pkg-a/src/a.js": 'export default "a"', + ".changeset/config.json": JSON.stringify({}), + }); + + // @ts-ignore + jest.spyOn(process, "exit").mockImplementation(() => {}); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + await fs.outputFile( + path.join(cwd, "packages/pkg-a/src/a.js"), + 'export default "updated a"' + ); + + await git.add(".", cwd); + await git.commit("updated a", cwd); + + await status( + cwd, + { since: "main" }, + { ...defaultConfig, changedFilePatterns: ["src/**"] } + ); + + expect(process.exit).toHaveBeenCalledWith(1); + }); + + it("should not exit early with a non-zero error code when there are changed packages matching the pattern and appropriate changeset", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + "packages/pkg-a/src/a.js": 'export default "a"', + ".changeset/config.json": JSON.stringify({}), + }); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); + await writeChangeset( + { + summary: "This is a summary", + releases: [{ name: "pkg-a", type: "minor" }], + }, + cwd + ); + await git.add(".", cwd); + await git.commit("updated a", cwd); + + const releaseObj = await status( + cwd, + { since: "main" }, + { ...defaultConfig, changedFilePatterns: ["src/**"] } + ); + expect(replaceHumanIds(releaseObj)).toMatchInlineSnapshot(` + { + "changesets": [ + { + "id": "~changeset-1~", + "releases": [ + { + "name": "pkg-a", + "type": "minor", + }, + ], + "summary": "This is a summary", + }, + ], + "preState": undefined, + "releases": [ + { + "changesets": [ + "~changeset-1~", ], "name": "pkg-a", "newVersion": "1.1.0", diff --git a/packages/cli/src/commands/status/index.ts b/packages/cli/src/commands/status/index.ts index ad4937278..7907cd30c 100644 --- a/packages/cli/src/commands/status/index.ts +++ b/packages/cli/src/commands/status/index.ts @@ -41,6 +41,7 @@ export default async function getStatus( const changedPackages = await git.getChangedPackagesSinceRef({ cwd, ref: sinceBranch || config.baseBranch, + changedFilePatterns: config.changedFilePatterns, }); if (changedPackages.length > 0 && changesets.length === 0) { diff --git a/packages/config/schema.json b/packages/config/schema.json index e7eb04e8b..459e45106 100644 --- a/packages/config/schema.json +++ b/packages/config/schema.json @@ -100,6 +100,14 @@ "default": "master", "description": "Determines the branch that Changesets uses when finding what packages have changed." }, + "changedFilePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "default": ["**"], + "description": "Glob patterns for changed files that should determining the package as changed." + }, "ignore": { "type": "array", "items": { diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts index 668a95d3c..b8b7f8835 100644 --- a/packages/config/src/index.test.ts +++ b/packages/config/src/index.test.ts @@ -45,6 +45,7 @@ test("read reads the config", async () => { commit: ["@changesets/cli/commit", { skipCI: "version" }], access: "restricted", baseBranch: "master", + changedFilePatterns: ["**"], updateInternalDependencies: "patch", ignore: [], bumpVersionsWithWorkspaceProtocolOnly: false, @@ -70,6 +71,7 @@ let defaults: Config = { commit: false, access: "restricted", baseBranch: "master", + changedFilePatterns: ["**"], updateInternalDependencies: "patch", ignore: [], privatePackages: { version: true, tag: false }, @@ -161,6 +163,15 @@ let correctCases: Record = { access: "public", }, }, + changedFilePatterns: { + input: { + changedFilePatterns: ["src/**"], + }, + output: { + ...defaults, + changedFilePatterns: ["src/**"], + }, + }, fixed: { input: { fixed: [["pkg-a", "pkg-b"]], @@ -294,6 +305,18 @@ let correctCases: Record = { ignore: ["pkg-a", "@pkg/a", "@pkg/b"], }, }, + privatePackagesFalseDisablesAll: { + input: { + privatePackages: false, + }, + output: { + ...defaults, + privatePackages: { + version: false, + tag: false, + }, + }, + }, updateInternalDependents: { input: { ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: { @@ -649,10 +672,23 @@ describe("parser errors", () => { `); }); - test("privatePackages false disables versioning and tagging", () => { - expect(unsafeParse({ privatePackages: false }, defaultPackages)).toEqual({ - ...defaults, - privatePackages: { version: false, tag: false }, - }); + test("changed files patterns - non-array", () => { + expect(() => unsafeParse({ changedFilePatterns: false }, defaultPackages)) + .toThrowErrorMatchingInlineSnapshot(` + "Some errors occurred when validating the changesets config: + The \`changedFilePatterns\` option is set as false but the \`changedFilePatterns\` option can only be set as an array of strings" + `); + }); + + test("changed files patterns - non-string element", () => { + expect(() => + unsafeParse({ changedFilePatterns: ["src/**", 100] }, defaultPackages) + ).toThrowErrorMatchingInlineSnapshot(` + "Some errors occurred when validating the changesets config: + The \`changedFilePatterns\` option is set as [ + "src/**", + 100 + ] but the \`changedFilePatterns\` option can only be set as an array of strings" + `); }); }); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 0f2468394..07e913ec2 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -170,6 +170,20 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => { ); } + if ( + json.changedFilePatterns !== undefined && + (!isArray(json.changedFilePatterns) || + !json.changedFilePatterns.every((pattern) => typeof pattern === "string")) + ) { + messages.push( + `The \`changedFilePatterns\` option is set as ${JSON.stringify( + json.changedFilePatterns, + null, + 2 + )} but the \`changedFilePatterns\` option can only be set as an array of strings` + ); + } + let fixed: string[][] = []; if (json.fixed !== undefined) { if (!havePackageGroupsCorrectShape(json.fixed)) { @@ -424,6 +438,8 @@ export let parse = (json: WrittenConfig, packages: Packages): Config => { ? defaultWrittenConfig.baseBranch : json.baseBranch, + changedFilePatterns: json.changedFilePatterns ?? ["**"], + updateInternalDependencies: json.updateInternalDependencies === undefined ? defaultWrittenConfig.updateInternalDependencies diff --git a/packages/git/package.json b/packages/git/package.json index 4696cf039..a4025c24a 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -12,11 +12,13 @@ "@changesets/types": "^5.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", + "micromatch": "^4.0.2", "spawndamnit": "^2.0.0" }, "devDependencies": { "@changesets/test-utils": "*", "@changesets/write": "*", - "file-url": "^3.0.0" + "file-url": "^3.0.0", + "fs-extra": "^7.0.1" } } diff --git a/packages/git/src/index.test.ts b/packages/git/src/index.test.ts index aaf8327ca..1aaab4d08 100644 --- a/packages/git/src/index.test.ts +++ b/packages/git/src/index.test.ts @@ -1,7 +1,8 @@ import path from "path"; +import fs from "fs-extra"; import spawn from "spawndamnit"; import fileUrl from "file-url"; -import { Fixture, tempdir, testdir } from "@changesets/test-utils"; +import { gitdir, tempdir } from "@changesets/test-utils"; import writeChangeset from "@changesets/write"; import { @@ -23,45 +24,18 @@ async function getCommitCount(cwd: string) { return parseInt(cmd.stdout.toString(), 10); } -async function setupGitDir(dir: Fixture) { - const cwd = await testdir(dir); - await spawn("git", ["init"], { cwd }); - // so that this works regardless of what the default branch of git init is and for git versions that don't support --initial-branch(like our CI) - { - const { stdout } = await spawn( - "git", - ["rev-parse", "--abbrev-ref", "HEAD"], - { cwd } - ); - if (stdout.toString("utf8").trim() !== "main") { - await spawn("git", ["checkout", "-b", "main"], { cwd }); - } - } - await spawn("git", ["config", "user.email", "x@y.z"], { cwd }); - await spawn("git", ["config", "user.name", "xyz"], { cwd }); - await spawn("git", ["config", "commit.gpgSign", "false"], { cwd }); - await spawn("git", ["config", "tag.gpgSign", "false"], { cwd }); - await spawn("git", ["config", "tag.forceSignAnnotated", "false"], { - cwd, - }); - - return cwd; -} - describe("git", () => { describe("getDivergedCommit", () => { it("should return same commit when branches have not diverged", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); const firstSha = await getCurrentCommitId({ cwd }); - await add("b.js", cwd); - await commit("added b.js", cwd); + await fs.outputFile(path.join(cwd, "b.js"), 'export default "updated b"'); + await commit("update b", cwd); const secondSha = await getCurrentCommitId({ cwd }); const divergedSha = await getDivergedCommit(cwd, "main"); @@ -70,21 +44,19 @@ describe("git", () => { }); it("should find commit where branch diverged", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); - // This is the first commit. We branch (diverge) from here. const mainSha = await getCurrentCommitId({ cwd }); // Create a new branch, and add a commit to it. await spawn("git", ["checkout", "-b", "my-branch"], { cwd }); - await add("b.js", cwd); - await commit("added b.js", cwd); + + await fs.outputFile(path.join(cwd, "b.js"), 'export default "updated b"'); + await commit("update b", cwd); // Now, get the latest commit from our new branch. const branchSha = await getCurrentCommitId({ cwd }); @@ -98,10 +70,12 @@ describe("git", () => { describe("add", () => { it("should add a file to the staging area", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', }); + + await fs.outputFile(path.join(cwd, "a.js"), 'export default "updated a"'); await add("a.js", cwd); const gitCmd = await spawn("git", ["diff", "--name-only", "--cached"], { @@ -116,11 +90,14 @@ describe("git", () => { }); it("should add multiple files to the staging area", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', "c.js": 'export default "c"', }); + + await fs.outputFile(path.join(cwd, "a.js"), 'export default "updated a"'); + await fs.outputFile(path.join(cwd, "c.js"), 'export default "updated c"'); await add("a.js", cwd); await add("c.js", cwd); @@ -138,10 +115,19 @@ describe("git", () => { }); it("should add a directory", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "foo/a.js": 'export default "a"', "foo/b.js": 'export default "b"', }); + + await fs.outputFile( + path.join(cwd, "foo/a.js"), + 'export default "updated a"' + ); + await fs.outputFile( + path.join(cwd, "foo/b.js"), + 'export default "updated b"' + ); await add("foo", cwd); const gitCmd = await spawn("git", ["diff", "--name-only", "--cached"], { @@ -158,28 +144,28 @@ describe("git", () => { describe("commit", () => { it("should commit a file", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); + + await fs.outputFile(path.join(cwd, "a.js"), 'export default "updated a"'); await add("a.js", cwd); - await commit("added a.js", cwd); + await commit("update a.js", cwd); const gitCmd = await spawn("git", ["log", "-1", "--pretty=%B"], { cwd, }); const commitMessage = gitCmd.stdout.toString().trim(); - expect(commitMessage).toEqual("added a.js"); + expect(commitMessage).toEqual("update a.js"); }); }); describe("getAllTags", () => { it("should retrieve all git tags", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); await tag("test_tag", cwd); await tag("test_tag2", cwd); @@ -191,11 +177,9 @@ describe("git", () => { describe("tag", () => { it("should create a tag for the current head", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); const head = await spawn("git", ["rev-parse", "HEAD"], { cwd }); await tag("tag_message", cwd); @@ -210,19 +194,20 @@ describe("git", () => { }); it("should create a tag, make a new commit, then create a second tag", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); const initialHead = await spawn("git", ["rev-parse", "HEAD"], { cwd, }); await tag("tag_message", cwd); + + await fs.outputFile(path.join(cwd, "b.js"), 'export default "updated b"'); await add("b.js", cwd); - await commit("added b.js", cwd); + await commit("update b", cwd); + const newHead = await spawn("git", ["rev-parse", "HEAD"], { cwd }); await tag("new_tag", cwd); @@ -245,21 +230,17 @@ describe("git", () => { describe("tagExists", () => { it("returns false when no tag exists", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); expect(await tagExists("tag_which_doesn't_exist", cwd)).toBe(false); }); it("returns true when tag exists", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); await tag("tag_message", cwd); @@ -268,15 +249,12 @@ describe("git", () => { }); describe("getCommitsThatAddFiles", () => { - it("should commit a file and get the hash of that commit", async () => { - const cwd = await setupGitDir({ + it("should get the hash of the commit that added a file", async () => { + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); const headSha = await getCurrentCommitId({ cwd }); - const commitHash = await getCommitsThatAddFiles(["a.js"], { cwd }); expect(commitHash).toEqual([headSha]); @@ -325,7 +303,7 @@ describe("git", () => { } it("reads the SHA of a file-add without deepening if commit already included in the shallow clone", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); @@ -333,13 +311,14 @@ describe("git", () => { // the commit we're going to scan for is the latest commit, // so will be in the shallow clone immediately without deepening await createDummyCommits(10, cwd); - const originalCommit = await addFileAndCommit("a.js", cwd); + await fs.outputFile(path.join(cwd, "b.js"), 'export default "b"'); + const originalCommit = await addFileAndCommit("b.js", cwd); const clone = await createShallowClone(5, cwd); // This file was added in the head commit, so will definitely be in our // 1-commit clone. - const commits = await getCommitsThatAddFiles(["a.js"], { cwd: clone }); + const commits = await getCommitsThatAddFiles(["b.js"], { cwd: clone }); expect(commits).toEqual([originalCommit]); // We should not need to have deepened the clone for this @@ -347,21 +326,22 @@ describe("git", () => { }); it("reads the SHA of a file-add even if not already included in the shallow clone", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); // We're going to create a repo where the commit we're looking for isn't // in the shallow clone, so we'll need to deepen it to locate it. await createDummyCommits((shallowCloneDeepeningAmount * 2) / 3, cwd); - const originalCommit = await addFileAndCommit("a.js", cwd); + await fs.outputFile(path.join(cwd, "b.js"), 'export default "b"'); + const originalCommit = await addFileAndCommit("b.js", cwd); await createDummyCommits((shallowCloneDeepeningAmount * 2) / 3, cwd); const clone = await createShallowClone(5, cwd); // Finding this commit will require deepening the clone until it appears. const commit = ( - await getCommitsThatAddFiles(["a.js"], { cwd: clone }) + await getCommitsThatAddFiles(["b.js"], { cwd: clone }) )[0]; expect(commit).toEqual(originalCommit); @@ -372,13 +352,13 @@ describe("git", () => { }); it("reads the SHA of a file-add even if the first commit of a repo", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); // Finding this commit will require deepening the clone right to the start // of the repo history, and coping with a commit that has no parent. - const originalCommit = await addFileAndCommit("a.js", cwd); + const originalCommit = await getCurrentCommitId({ cwd }); await createDummyCommits(shallowCloneDeepeningAmount * 2, cwd); const clone = await createShallowClone(5, cwd); @@ -394,22 +374,26 @@ describe("git", () => { }); it("can return SHAs for multiple files including return blanks for missing files", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', - "b.js": 'export default "b"', }); // We want to ensure that we can retrieve SHAs for multiple files at the same time, // and also that requesting missing files doesn't affect the location of commits // for the files that succeed. await createDummyCommits(shallowCloneDeepeningAmount, cwd); - const originalCommit1 = await addFileAndCommit("a.js", cwd); + + await fs.outputFile(path.join(cwd, "b.js"), 'export default "b"'); + const originalCommit1 = await addFileAndCommit("b.js", cwd); + await createDummyCommits(shallowCloneDeepeningAmount, cwd); - const originalCommit2 = await addFileAndCommit("b.js", cwd); + + await fs.outputFile(path.join(cwd, "c.js"), 'export default "c"'); + const originalCommit2 = await addFileAndCommit("c.js", cwd); const clone = await createShallowClone(5, cwd); const commits = await getCommitsThatAddFiles( - ["a.js", "this-file-does-not-exist", "b.js"], + ["b.js", "this-file-does-not-exist", "c.js"], { cwd: clone } ); @@ -420,57 +404,52 @@ describe("git", () => { describe("getChangedFilesSince", () => { it("should be empty if no changes (partial path)", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); - const head = await getCurrentCommitId({ cwd }); const changedFiles = await getChangedFilesSince({ ref: head, cwd, fullPath: false, }); + expect(changedFiles).toHaveLength(0); }); it("should be empty if no changes (full path)", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); - const head = await getCurrentCommitId({ cwd }); const changedFiles = await getChangedFilesSince({ ref: head, cwd, fullPath: true, }); + expect(changedFiles).toHaveLength(0); }); it("should get list of files that have been committed", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "a.js": 'export default "a"', "b.js": 'export default "b"', "c.js": 'export default "c"', "d.js": 'export default "d"', }); - await add("a.js", cwd); - await commit("added a.js", cwd); - const firstRef = await getCurrentCommitId({ cwd }); + await fs.outputFile(path.join(cwd, "b.js"), 'export default "updated b"'); await add("b.js", cwd); - await commit("added b.js", cwd); + await commit("update b.js", cwd); const secondRef = await getCurrentCommitId({ cwd }); + await fs.outputFile(path.join(cwd, "d.js"), 'export default "updated d"'); await add("d.js", cwd); - await commit("added d.js", cwd); + await commit("update d.js", cwd); const filesChangedSinceFirstRef = await getChangedFilesSince({ ref: firstRef, @@ -485,22 +464,27 @@ describe("git", () => { expect(filesChangedSinceSecondRef).toEqual(["d.js"]); }); it("should get correct full paths of changed files irrespective of cwd", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "packages/pkg-a/a.js": 'export default "a"', "packages/pkg-b/b.js": 'export default "b"', "packages/pkg-c/c.js": 'export default "c"', }); - await add("packages/pkg-a/a.js", cwd); - await commit("added a.js", cwd); - const ref = await getCurrentCommitId({ cwd }); + await fs.outputFile( + path.join(cwd, "packages/pkg-b/b.js"), + 'export default "updated b"' + ); await add("packages/pkg-b/b.js", cwd); - await commit("added b.js", cwd); + await commit("update b.js", cwd); + await fs.outputFile( + path.join(cwd, "packages/pkg-c/c.js"), + 'export default "updated c"' + ); await add("packages/pkg-c/c.js", cwd); - await commit("added c.js", cwd); + await commit("update c.js", cwd); const filesChangedSinceRef = await getChangedFilesSince({ ref, @@ -526,7 +510,7 @@ describe("git", () => { describe("getChangedPackagesSinceRef", () => { it("should return an empty list if no packages have changed", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -537,8 +521,12 @@ describe("git", () => { "packages/pkg-a/a.js": 'export default "a"', }); + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "updated a"' + ); await add("packages/pkg-a/a.js", cwd); - await commit("added a.js", cwd); + await commit("update a.js", cwd); await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); const changedPackages = await getChangedPackagesSinceRef({ @@ -549,7 +537,7 @@ describe("git", () => { }); it("should check changed packages on a branch against base branch", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -568,15 +556,25 @@ describe("git", () => { }), }); - await add("packages/pkg-a/package.json", cwd); - await commit("added pkg-a", cwd); - await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); - await add("packages/pkg-b/package.json", cwd); - await commit("added pkg-b", cwd); - await add("packages/pkg-d/package.json", cwd); - await commit("added pkg-d", cwd); + await fs.outputFile( + path.join(cwd, "packages/pkg-b/package.json"), + JSON.stringify({ + name: "pkg-b", + private: true, + }) + ); + await commit("update pkg-b", cwd); + + await fs.outputFile( + path.join(cwd, "packages/pkg-d/package.json"), + JSON.stringify({ + name: "pkg-d", + private: true, + }) + ); + await commit("update pkg-d", cwd); const changedPackages = await getChangedPackagesSinceRef({ cwd, @@ -588,11 +586,149 @@ describe("git", () => { "pkg-d", ]); }); + + it("should return the closest package for a changed file (shorter workspace pattern before a longer one)", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*", "packages/*/examples/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + }), + "packages/pkg-a/examples/example-a/package.json": JSON.stringify({ + name: "example-a", + }), + }); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + const newFilePath = "packages/pkg-a/examples/example-a/file.js"; + + await fs.outputFile( + path.join(cwd, newFilePath), + "console.log('hello world');" + ); + + await add(newFilePath, cwd); + await commit("new file in the example", cwd); + + const changedPackages = await getChangedPackagesSinceRef({ + cwd, + ref: "main", + }); + + expect(changedPackages.map((pkg) => pkg.packageJson.name)).toEqual([ + "example-a", + ]); + }); + + it("should return the closest package for a changed file (longer workspace pattern before a shorter one)", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*/examples/*", "packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + }), + "packages/pkg-a/examples/example-a/package.json": JSON.stringify({ + name: "example-a", + }), + }); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + const newFilePath = "packages/pkg-a/examples/example-a/file.js"; + + await fs.outputFile( + path.join(cwd, newFilePath), + "console.log('hello world');" + ); + + await add(newFilePath, cwd); + await commit("new file in the example", cwd); + + const changedPackages = await getChangedPackagesSinceRef({ + cwd, + ref: "main", + }); + + expect(changedPackages.map((pkg) => pkg.packageJson.name)).toEqual([ + "example-a", + ]); + }); + + it("should not return package as changed when a file not matching the changed pattern has been changed", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + }), + }); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + const newFilePath = "packages/pkg-a/__tests__/file.js"; + + await fs.outputFile( + path.join(cwd, newFilePath), + "expect(answer).toBe(42);" + ); + + await add(newFilePath, cwd); + await commit("new test file", cwd); + + const changedPackages = await getChangedPackagesSinceRef({ + cwd, + ref: "main", + changedFilePatterns: ["src/**"], + }); + + expect(changedPackages.map((pkg) => pkg.packageJson.name)).toEqual([]); + }); + + it("should return package as changed when a file matching the changed pattern has been changed", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + }), + }); + + await spawn("git", ["checkout", "-b", "new-branch"], { cwd }); + + const newFilePath = "packages/pkg-a/src/index.js"; + + await fs.outputFile( + path.join(cwd, newFilePath), + "export const answer = 42;" + ); + + await add(newFilePath, cwd); + await commit("awesome implementation file", cwd); + + const changedPackages = await getChangedPackagesSinceRef({ + cwd, + ref: "main", + changedFilePatterns: ["src/**"], + }); + + expect(changedPackages.map((pkg) => pkg.packageJson.name)).toEqual([ + "pkg-a", + ]); + }); }); describe("getChangedChangesetFilesSinceRef", () => { it("should be empty if no changeset files have been added", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -603,9 +739,6 @@ describe("git", () => { ".changeset/config.json": JSON.stringify({}), }); - await add("packages/pkg-a/package.json", cwd); - await commit("added pkg-a", cwd); - const files = await getChangedChangesetFilesSinceRef({ cwd, ref: "main", @@ -614,7 +747,7 @@ describe("git", () => { }); it("should get the relative path to the changeset file", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -625,9 +758,6 @@ describe("git", () => { ".changeset/config.json": JSON.stringify({}), }); - await add("packages/pkg-a/package.json", cwd); - await commit("added pkg-a", cwd); - const changesetId = await writeChangeset( { releases: [ @@ -649,7 +779,7 @@ describe("git", () => { expect(files).toEqual([`.changeset/${changesetId}.md`]); }); it("should work on a ref that isn't the base branch", async () => { - const cwd = await setupGitDir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -661,8 +791,6 @@ describe("git", () => { }); await spawn("git", ["checkout", "-b", "some-branch"], { cwd }); - await add("packages/pkg-a/package.json", cwd); - await commit("added pkg-a", cwd); const changesetId = await writeChangeset( { diff --git a/packages/git/src/index.ts b/packages/git/src/index.ts index b08f2611f..7e3aee534 100644 --- a/packages/git/src/index.ts +++ b/packages/git/src/index.ts @@ -4,8 +4,7 @@ import path from "path"; import { getPackages, Package } from "@manypkg/get-packages"; import { GitError } from "@changesets/errors"; import isSubdir from "is-subdir"; - -const isInDir = (dir: string) => (subdir: string) => isSubdir(dir, subdir); +import micromatch from "micromatch"; export async function add(pathToFile: string, cwd: string) { const gitCmd = await spawn("git", ["add", pathToFile], { cwd }); @@ -250,26 +249,36 @@ export async function getChangedChangesetFilesSinceRef({ export async function getChangedPackagesSinceRef({ cwd, ref, + changedFilePatterns = ["**"], }: { cwd: string; ref: string; -}) { + changedFilePatterns?: readonly string[]; +}): Promise { const changedFiles = await getChangedFilesSince({ ref, cwd, fullPath: true }); - let packages = await getPackages(cwd); - - const fileToPackage: Record = {}; - - packages.packages.forEach((pkg) => - changedFiles.filter(isInDir(pkg.dir)).forEach((fileName) => { - const prevPkg = fileToPackage[fileName] || { dir: "" }; - if (pkg.dir.length > prevPkg.dir.length) fileToPackage[fileName] = pkg; - }) - ); return ( - Object.values(fileToPackage) - // filter, so that we have only unique packages - .filter((pkg, idx, packages) => packages.indexOf(pkg) === idx) + [...(await getPackages(cwd)).packages] + // sort packages by length of dir, so that we can check for subdirs first + .sort((pkgA, pkgB) => pkgB.dir.length - pkgA.dir.length) + .filter((pkg) => { + const changedPackageFiles: string[] = []; + + for (let i = changedFiles.length - 1; i >= 0; i--) { + const file = changedFiles[i]; + + if (isSubdir(pkg.dir, file)) { + changedFiles.splice(i, 1); + const relativeFile = file.slice(pkg.dir.length + 1); + changedPackageFiles.push(relativeFile); + } + } + + return ( + changedPackageFiles.length > 0 && + micromatch(changedPackageFiles, changedFilePatterns).length > 0 + ); + }) ); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 13fdad1a0..17bb849eb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -73,6 +73,7 @@ export type Config = { linked: Linked; access: AccessType; baseBranch: string; + changedFilePatterns: readonly string[]; /** Features enabled for Private packages */ privatePackages: PrivatePackages; /** The minimum bump type to trigger automatic update of internal dependencies that are part of the same release */ @@ -97,6 +98,7 @@ export type WrittenConfig = { linked?: Linked; access?: AccessType; baseBranch?: string; + changedFilePatterns?: readonly string[]; /** Opt in to tracking non-npm / private packages */ privatePackages?: | false diff --git a/scripts/test-utils/package.json b/scripts/test-utils/package.json index 067b48d7e..9fe1867f1 100644 --- a/scripts/test-utils/package.json +++ b/scripts/test-utils/package.json @@ -10,6 +10,7 @@ "dependencies": { "@changesets/logger": "^0.0.5", "fixturez": "^1.1.0", - "fs-extra": "^7.0.1" + "fs-extra": "^7.0.1", + "spawndamnit": "^2.0.0" } } diff --git a/scripts/test-utils/src/index.ts b/scripts/test-utils/src/index.ts index da878a06b..26c699f43 100644 --- a/scripts/test-utils/src/index.ts +++ b/scripts/test-utils/src/index.ts @@ -1,4 +1,5 @@ import fixturez from "fixturez"; +import spawn from "spawndamnit"; import path from "path"; import fs from "fs-extra"; @@ -102,3 +103,33 @@ export async function testdir(dir: Fixture) { } export const tempdir = f.temp; + +export async function gitdir(dir: Fixture) { + const cwd = await testdir(dir); + await spawn("git", ["init"], { cwd }); + // so that this works regardless of what the default branch of git init is and for git versions that don't support --initial-branch(like our CI) + { + const { stdout } = await spawn( + "git", + ["rev-parse", "--abbrev-ref", "HEAD"], + { cwd } + ); + if (stdout.toString("utf8").trim() !== "main") { + await spawn("git", ["checkout", "-b", "main"], { cwd }); + } + } + await spawn("git", ["config", "user.email", "x@y.z"], { cwd }); + await spawn("git", ["config", "user.name", "xyz"], { cwd }); + await spawn("git", ["config", "commit.gpgSign", "false"], { cwd }); + await spawn("git", ["config", "tag.gpgSign", "false"], { cwd }); + await spawn("git", ["config", "tag.forceSignAnnotated", "false"], { + cwd, + }); + + await spawn("git", ["add", "."], { cwd }); + await spawn("git", ["commit", "-m", "initial commit", "--allow-empty"], { + cwd, + }); + + return cwd; +}