From 87893ccfbf993dc7cff8414eeeb948661266abc6 Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Tue, 18 May 2021 11:20:49 -0700 Subject: [PATCH 1/3] feat: podspecPath can be an array to push multiple podspecs with the cocoapod plugin --- plugins/cocoapods/README.md | 23 ++ plugins/cocoapods/__tests__/cocoapods.test.ts | 326 +++++++++++++++++- plugins/cocoapods/src/index.ts | 117 +++++-- 3 files changed, 411 insertions(+), 55 deletions(-) diff --git a/plugins/cocoapods/README.md b/plugins/cocoapods/README.md index b492397cf..e3b3119bd 100644 --- a/plugins/cocoapods/README.md +++ b/plugins/cocoapods/README.md @@ -38,6 +38,29 @@ yarn add -D @auto-it/cocoapods } ``` +Or with multiple podspecs: + +```json +{ + "plugins": [ + [ + "cocoapods", + { + // Required, the relative path to your podspec file + "podspecPath": ["./Test.podspec", "./Test2.podspec"], + // Optional, the specs repo to push to + "specsRepo": "https://github.com/intuit/TestSpecs.git", + // Optional, flags to pass to the `pod repo push` command + "flags": ["--sources=https://github.com/SpecRepo.git"], + // Optional, specify a different executable for `pod` + "podCommand": "bundle exec pod" + } + ] + // other plugins + ] +} +``` + ## Requirements ### General diff --git a/plugins/cocoapods/__tests__/cocoapods.test.ts b/plugins/cocoapods/__tests__/cocoapods.test.ts index d016278f5..7edb5c82f 100644 --- a/plugins/cocoapods/__tests__/cocoapods.test.ts +++ b/plugins/cocoapods/__tests__/cocoapods.test.ts @@ -51,6 +51,18 @@ const mockPodspec = (contents: string) => { return jest.spyOn(utilities, "getPodspecContents").mockReturnValue(contents); }; +interface FakePodspec { + path: string; + contents: string; +} +const mockPodspecs = (contents: FakePodspec[]) => { + return jest + .spyOn(utilities, "getPodspecContents") + .mockImplementation((path) => { + return contents.find((podspec) => podspec.path === path).contents; + }); +}; + let exec = jest.fn(); // @ts-ignore jest.mock("../../../packages/core/dist/utils/exec-promise", () => (...args) => @@ -60,6 +72,7 @@ const logger = dummyLog(); describe("Cocoapods Plugin", () => { let hooks: Auto.IAutoHooks; + let multiHooks: Auto.IAutoHooks; const prefixRelease: (a: string) => string = (version: string) => { return `v${version}`; }; @@ -68,31 +81,42 @@ describe("Cocoapods Plugin", () => { podspecPath: "./Test.podspec", }; + const multiOptions: ICocoapodsPluginOptions = { + podspecPath: ["./Test.podspec", "./Test2.podspec"], + }; + beforeEach(() => { jest.resetAllMocks(); exec.mockClear(); const plugin = new CocoapodsPlugin(options); + const multiPlugin = new CocoapodsPlugin(multiOptions); hooks = makeHooks(); - plugin.apply(({ - hooks, - logger: logger, - prefixRelease, - git: { - getLastTagNotInBaseBranch: async () => undefined, - getLatestRelease: async () => "0.0.1", - getPullRequest: async () => ({ - data: { - head: { - repo: { - clone_url: "https://github.com/intuit-fork/auto.git", + multiHooks = makeHooks(); + const apply = (p: CocoapodsPlugin, h: Auto.IAutoHooks) => { + p.apply(({ + hooks: h, + logger: logger, + prefixRelease, + git: { + getLastTagNotInBaseBranch: async () => undefined, + getLatestRelease: async () => "0.0.1", + getPullRequest: async () => ({ + data: { + head: { + repo: { + clone_url: "https://github.com/intuit-fork/auto.git", + }, }, }, - }, - }), - }, - remote: "https://github.com/intuit/auto.git", - getCurrentVersion: async () => "0.0.1", - } as unknown) as Auto.Auto); + }), + }, + remote: "https://github.com/intuit/auto.git", + getCurrentVersion: async () => "0.0.1", + } as unknown) as Auto.Auto); + }; + + apply(plugin, hooks); + apply(multiPlugin, multiHooks); }); describe("getParsedPodspecContents", () => { @@ -224,6 +248,20 @@ describe("Cocoapods Plugin", () => { "Version could not be found in podspec: ./Test.podspec" ); }); + + test("should get version if multiple podspecs", async () => { + mockPodspecs([ + { + path: "./Test.podspec", + contents: specWithVersion("0.0.1"), + }, + { + path: "./Test2.podspec", + contents: specWithVersion("0.0.1"), + }, + ]); + expect(await multiHooks.getPreviousVersion.promise()).toBe("v0.0.1"); + }); }); describe("version hook", () => { test("should do nothing on dryRun", async () => { @@ -279,6 +317,81 @@ describe("Cocoapods Plugin", () => { expect(mock).lastCalledWith(expect.any(String), specWithVersion("1.0.0")); }); + test("should release version multiple podspecs - patch", async () => { + mockPodspecs([ + { + path: "./Test.podspec", + contents: specWithVersion("0.0.1"), + }, + { + path: "./Test2.podspec", + contents: specWithVersion("0.0.1"), + }, + ]); + const mock = jest.spyOn(utilities, "writePodspecContents"); + await multiHooks.version.promise({ bump: Auto.SEMVER.patch }); + expect(mock).toHaveBeenCalledTimes(2); + expect(mock).toHaveBeenNthCalledWith( + 1, + "./Test.podspec", + specWithVersion("0.0.2") + ); + expect(mock).toHaveBeenNthCalledWith( + 2, + "./Test2.podspec", + specWithVersion("0.0.2") + ); + }); + test("should release version multiple podspecs - minor", async () => { + mockPodspecs([ + { + path: "./Test.podspec", + contents: specWithVersion("0.0.1"), + }, + { + path: "./Test2.podspec", + contents: specWithVersion("0.0.1"), + }, + ]); + const mock = jest.spyOn(utilities, "writePodspecContents"); + await multiHooks.version.promise({ bump: Auto.SEMVER.minor }); + expect(mock).toHaveBeenCalledTimes(2); + expect(mock).toHaveBeenNthCalledWith( + 1, + "./Test.podspec", + specWithVersion("0.1.0") + ); + expect(mock).toHaveBeenNthCalledWith( + 2, + "./Test2.podspec", + specWithVersion("0.1.0") + ); + }); + test("should release version multiple podspecs - major", async () => { + mockPodspecs([ + { + path: "./Test.podspec", + contents: specWithVersion("0.0.1"), + }, + { + path: "./Test2.podspec", + contents: specWithVersion("0.0.1"), + }, + ]); + const mock = jest.spyOn(utilities, "writePodspecContents"); + await multiHooks.version.promise({ bump: Auto.SEMVER.major }); + expect(mock).toHaveBeenCalledTimes(2); + expect(mock).toHaveBeenNthCalledWith( + 1, + "./Test.podspec", + specWithVersion("1.0.0") + ); + expect(mock).toHaveBeenNthCalledWith( + 2, + "./Test2.podspec", + specWithVersion("1.0.0") + ); + }); test("should throw if there is an error writing new version", async () => { mockPodspec(specWithVersion("0.0.1")); @@ -318,6 +431,32 @@ describe("Cocoapods Plugin", () => { expect(exec).toBeCalledTimes(1); expect(exec).lastCalledWith("pod", ["lib", "lint", "./Test.podspec"]); }); + test("should call pod lib lint for each podspec with dryRun flag", async () => { + mockPodspec(specWithVersion("0.0.1")); + + const plugin = new CocoapodsPlugin(multiOptions); + const hook = makeHooks(); + + plugin.apply({ + hooks: hook, + logger: dummyLog(), + prefixRelease, + } as Auto.Auto); + + await hook.beforeShipIt.promise({ releaseType: "latest", dryRun: true }); + + expect(exec).toBeCalledTimes(2); + expect(exec).toHaveBeenNthCalledWith(1, "pod", [ + "lib", + "lint", + "./Test.podspec", + ]); + expect(exec).toHaveBeenNthCalledWith(2, "pod", [ + "lib", + "lint", + "./Test2.podspec", + ]); + }); test("should call pod lib lint with options with dryRun flag", async () => { mockPodspec(specWithVersion("0.0.1")); @@ -409,6 +548,46 @@ describe("Cocoapods Plugin", () => { ) ); }); + test("should tag multiple podspeccs with canary version", async () => { + jest.spyOn(Auto, "getPrNumberFromEnv").mockReturnValue(1); + const specs = { + "./Test.podspec": specWithVersion("0.0.1"), + "./Test2.podspec": specWithVersion("0.0.1"), + }; + jest + .spyOn(utilities, "getPodspecContents") + .mockImplementation((path) => specs[path]); + const mock = jest + .spyOn(utilities, "writePodspecContents") + .mockImplementation((path, contents) => { + specs[path] = contents; + }); + + const newVersion = await multiHooks.canary.promise({ + bump: "minor" as Auto.SEMVER, + canaryIdentifier: "canary.1.1.1", + }); + + expect(newVersion).toBe("0.1.0-canary.1.1.1"); + expect(exec).toBeCalledTimes(6); + expect(exec).toHaveBeenCalledWith("git", ["checkout", "./Test.podspec"]); + expect(exec).toHaveBeenCalledWith("git", ["checkout", "./Test2.podspec"]); + expect(mock).toBeCalledTimes(4); + expect(mock).toHaveBeenCalledWith( + "./Test.podspec", + specWithVersion( + "0.1.0-canary.1.1.1", + "{ :git => 'https://github.com/intuit-fork/auto.git', :commit => 'undefined' }" + ) + ); + expect(mock).toHaveBeenCalledWith( + "./Test2.podspec", + specWithVersion( + "0.1.0-canary.1.1.1", + "{ :git => 'https://github.com/intuit-fork/auto.git', :commit => 'undefined' }" + ) + ); + }); test("should tag with canary version with no PR number", async () => { let podSpec = specWithVersion("0.0.1"); jest @@ -453,6 +632,43 @@ describe("Cocoapods Plugin", () => { expect(versions).toStrictEqual(["0.0.1", "0.0.2", "0.1.0-next.0"]); }); test("should tag with next version", async () => { + jest.spyOn(Auto, "getCurrentBranch").mockReturnValue("next"); + const specs = { + "./Test.podspec": specWithVersion("0.0.1"), + "./Test2.podspec": specWithVersion("0.0.1"), + }; + jest + .spyOn(utilities, "getPodspecContents") + .mockImplementation((path) => specs[path]); + const mock = jest + .spyOn(utilities, "writePodspecContents") + .mockImplementation((path, contents) => { + specs[path] = contents; + }); + + const versions = await multiHooks.next.promise([], { + bump: Auto.SEMVER.major, + dryRun: false, + commits: [], + fullReleaseNotes: "", + releaseNotes: "", + }); + + expect(versions).toContain("1.0.0-next.0"); + expect(exec).toBeCalledTimes(6); + expect(exec).toHaveBeenCalledWith("git", ["checkout", "./Test.podspec"]); + + expect(mock).toBeCalledTimes(2); + expect(mock).toHaveBeenCalledWith( + "./Test.podspec", + specWithVersion("1.0.0-next.0") + ); + expect(mock).toHaveBeenCalledWith( + "./Test2.podspec", + specWithVersion("1.0.0-next.0") + ); + }); + test("should tag with next version for multiple podspecs", async () => { jest.spyOn(Auto, "getCurrentBranch").mockReturnValue("next"); let podSpec = specWithVersion("0.0.1"); jest @@ -500,6 +716,31 @@ describe("Cocoapods Plugin", () => { expect(exec).toBeCalledTimes(2); expect(exec).lastCalledWith("pod", ["trunk", "push", "./Test.podspec"]); }); + test("should push multiple podspecs to trunk if no specsRepo in options", async () => { + mockPodspec(specWithVersion("0.0.1")); + + const plugin = new CocoapodsPlugin(multiOptions); + const hook = makeHooks(); + plugin.apply({ + hooks: hook, + logger: dummyLog(), + prefixRelease, + } as Auto.Auto); + + await hook.publish.promise({ bump: Auto.SEMVER.patch }); + + expect(exec).toBeCalledTimes(3); + expect(exec).toHaveBeenCalledWith("pod", [ + "trunk", + "push", + "./Test.podspec", + ]); + expect(exec).toHaveBeenCalledWith("pod", [ + "trunk", + "push", + "./Test2.podspec", + ]); + }); test("should push with different pod command if in options", async () => { mockPodspec(specWithVersion("0.0.1")); @@ -620,6 +861,55 @@ describe("Cocoapods Plugin", () => { "--silent", ]); }); + test("should push multiple podspecs to specs repo if specsRepo in options", async () => { + mockPodspec(specWithVersion("0.0.1")); + + const logger = dummyLog(); + logger.logLevel = "quiet"; + + const plugin = new CocoapodsPlugin({ + ...multiOptions, + specsRepo: "someSpecsRepo", + }); + const hook = makeHooks(); + plugin.apply({ + hooks: hook, + logger, + prefixRelease, + } as Auto.Auto); + + await hook.publish.promise({ bump: Auto.SEMVER.patch }); + + expect(exec).toBeCalledTimes(6); + expect(exec).toHaveBeenNthCalledWith(2, "pod", ["repo", "list"]); + expect(exec).toHaveBeenNthCalledWith(3, "pod", [ + "repo", + "add", + "autoPublishRepo", + "someSpecsRepo", + "--silent", + ]); + expect(exec).toHaveBeenNthCalledWith(4, "pod", [ + "repo", + "push", + "autoPublishRepo", + "./Test.podspec", + "--silent", + ]); + expect(exec).toHaveBeenNthCalledWith(5, "pod", [ + "repo", + "push", + "autoPublishRepo", + "./Test2.podspec", + "--silent", + ]); + expect(exec).toHaveBeenNthCalledWith(6, "pod", [ + "repo", + "remove", + "autoPublishRepo", + "--silent", + ]); + }); test("should push to specs repo if specsRepo in options with flags", async () => { mockPodspec(specWithVersion("0.0.1")); diff --git a/plugins/cocoapods/src/index.ts b/plugins/cocoapods/src/index.ts index a483cfcba..8b91ce9e5 100644 --- a/plugins/cocoapods/src/index.ts +++ b/plugins/cocoapods/src/index.ts @@ -34,7 +34,7 @@ const logMessage = (msg: string): string => `${logPrefix} ${msg}`; const required = t.interface({ /** Relative path to podspec file */ - podspecPath: t.string, + podspecPath: t.union([t.string, t.array(t.string)]), }); const optional = t.partial({ @@ -161,6 +161,18 @@ export default class CocoapodsPlugin implements IPlugin { /** The options of the plugin */ readonly options: ICocoapodsPluginOptions; + /** + * + */ + private get paths() { + if (typeof this.options.podspecPath === "string") { + return [this.options.podspecPath]; + } + + return this.options.podspecPath; + + } + /** Initialize the plugin with it's options */ constructor(options: ICocoapodsPluginOptions) { this.options = options; @@ -186,16 +198,17 @@ export default class CocoapodsPlugin implements IPlugin { noVersionPrefix: true, })); - auto.hooks.getPreviousVersion.tapPromise(this.name, async () => - auto.prefixRelease(getVersion(this.options.podspecPath)) - ); + auto.hooks.getPreviousVersion.tapPromise(this.name, async () => { + // Due to CocoaPods being git backed, all the versions will be the same + // so there are no git tag collisions + return auto.prefixRelease(getVersion(this.paths[0])); + }); auto.hooks.version.tapPromise( this.name, async ({ bump, dryRun, quiet }) => { - const previousVersion = getVersion(this.options.podspecPath); + const previousVersion = getVersion(this.paths[0]); const releaseVersion = inc(previousVersion, bump as ReleaseType); - if (dryRun && releaseVersion) { if (quiet) { console.log(releaseVersion); @@ -212,7 +225,9 @@ export default class CocoapodsPlugin implements IPlugin { ); } - updatePodspecVersion(this.options.podspecPath, releaseVersion); + this.paths.forEach((path) => { + updatePodspecVersion(path, releaseVersion); + }); await execPromise("git", [ "commit", @@ -223,7 +238,7 @@ export default class CocoapodsPlugin implements IPlugin { await execPromise("git", [ "tag", - releaseVersion, + `${releaseVersion}`, "-m", `"Update version to ${releaseVersion}"`, ]); @@ -266,15 +281,23 @@ export default class CocoapodsPlugin implements IPlugin { return; } - await updateSourceLocation(this.options.podspecPath, remoteRepo); + await this.paths.reduce( + (promise, path) => + promise.then(async () => { + await updateSourceLocation(path, remoteRepo); - updatePodspecVersion(this.options.podspecPath, canaryVersion); + updatePodspecVersion(path, canaryVersion); + }), + Promise.resolve() + ); // Publish the canary podspec, committing it isn't needed for specs push await this.publishPodSpec(podLogLevel); // Reset changes to podspec file since it doesn't need to be committed - await execPromise("git", ["checkout", this.options.podspecPath]); + await Promise.all( + this.paths.map((path) => execPromise("git", ["checkout", path])) + ); return canaryVersion; } @@ -319,13 +342,15 @@ export default class CocoapodsPlugin implements IPlugin { await execPromise("git", ["push", auto.remote, branch, "--tags"]); - updatePodspecVersion(this.options.podspecPath, prerelease); + this.paths.forEach((path) => updatePodspecVersion(path, prerelease)); // Publish the next podspec, committing it isn't needed for specs push await this.publishPodSpec(podLogLevel); // Reset changes to podspec file since it doesn't need to be committed - await execPromise("git", ["checkout", this.options.podspecPath]); + await Promise.all( + this.paths.map((path) => execPromise("git", ["checkout", path])) + ); return preReleaseVersions; } @@ -337,13 +362,19 @@ export default class CocoapodsPlugin implements IPlugin { const [pod, ...commands] = this.options.podCommand?.split(" ") || [ "pod", ]; - await execPromise(pod, [ - ...commands, - "lib", - "lint", - ...(this.options.flags || []), - this.options.podspecPath, - ]); + await this.paths.reduce( + (promise, path) => + promise.then(() => + execPromise(pod, [ + ...commands, + "lib", + "lint", + ...(this.options.flags || []), + path, + ]) + ), + Promise.resolve() + ); } }); @@ -366,14 +397,20 @@ export default class CocoapodsPlugin implements IPlugin { const [pod, ...commands] = this.options.podCommand?.split(" ") || ["pod"]; if (!this.options.specsRepo) { this.logger?.log.info(logMessage(`Pushing to Cocoapods trunk`)); - await execPromise(pod, [ - ...commands, - "trunk", - "push", - ...(this.options.flags || []), - this.options.podspecPath, - ...podLogLevel, - ]); + await this.paths.reduce( + (promise, path) => + promise.then(() => + execPromise(pod, [ + ...commands, + "trunk", + "push", + ...(this.options.flags || []), + path, + ...podLogLevel, + ]) + ), + Promise.resolve() + ); return; } @@ -413,15 +450,21 @@ export default class CocoapodsPlugin implements IPlugin { logMessage(`Pushing to specs repo: ${this.options.specsRepo}`) ); - await execPromise(pod, [ - ...commands, - "repo", - "push", - ...(this.options.flags || []), - "autoPublishRepo", - this.options.podspecPath, - ...podLogLevel, - ]); + await this.paths.reduce( + (promise, path) => + promise.then(() => + execPromise(pod, [ + ...commands, + "repo", + "push", + ...(this.options.flags || []), + "autoPublishRepo", + path, + ...podLogLevel, + ]) + ), + Promise.resolve() + ); } catch (error) { this.logger?.log.error( logMessage( From 62b93c5ef438c9541294206a03c2d47d7d3b6bcc Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Tue, 18 May 2021 11:34:11 -0700 Subject: [PATCH 2/3] fix: fix promise.resolve initial value --- plugins/cocoapods/src/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/cocoapods/src/index.ts b/plugins/cocoapods/src/index.ts index 8b91ce9e5..75209b695 100644 --- a/plugins/cocoapods/src/index.ts +++ b/plugins/cocoapods/src/index.ts @@ -168,9 +168,8 @@ export default class CocoapodsPlugin implements IPlugin { if (typeof this.options.podspecPath === "string") { return [this.options.podspecPath]; } - - return this.options.podspecPath; - + + return this.options.podspecPath; } /** Initialize the plugin with it's options */ @@ -373,7 +372,7 @@ export default class CocoapodsPlugin implements IPlugin { path, ]) ), - Promise.resolve() + Promise.resolve("") ); } }); @@ -409,7 +408,7 @@ export default class CocoapodsPlugin implements IPlugin { ...podLogLevel, ]) ), - Promise.resolve() + Promise.resolve("") ); return; } @@ -463,7 +462,7 @@ export default class CocoapodsPlugin implements IPlugin { ...podLogLevel, ]) ), - Promise.resolve() + Promise.resolve("") ); } catch (error) { this.logger?.log.error( From 5dfa5a7abd849ab5811a20f2b6f8729c1e49f358 Mon Sep 17 00:00:00 2001 From: Harris Borawski Date: Tue, 18 May 2021 13:04:33 -0700 Subject: [PATCH 3/3] add some config validation tests --- plugins/cocoapods/__tests__/cocoapods.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/cocoapods/__tests__/cocoapods.test.ts b/plugins/cocoapods/__tests__/cocoapods.test.ts index 7edb5c82f..bd25574b6 100644 --- a/plugins/cocoapods/__tests__/cocoapods.test.ts +++ b/plugins/cocoapods/__tests__/cocoapods.test.ts @@ -226,6 +226,21 @@ describe("Cocoapods Plugin", () => { ); }); }); + describe("validateConfig hook", () => { + test("should validate options", async () => { + expect( + ((await hooks.validateConfig.promise("cocoapods", {})) || []) + ).toHaveLength(1); + expect( + ((await hooks.validateConfig.promise("cocoapods", options)) || []) + + ).toHaveLength(0); + expect( + ((await hooks.validateConfig.promise("cocoapods", multiOptions)) || []) + + ).toHaveLength(0); + }); + }); describe("modifyConfig hook", () => { test("should set noVersionPrefix to true", async () => { const config = {};