import path from "node:path"; import * as core from "@actions/core"; import type { Changeset } from "@changesets/types"; import { writeChangeset } from "@changesets/write"; import { createFixture } from "fs-fixture"; import { exec } from "tinyexec"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { GitHub } from "./github.ts"; import { runPublish, runVersion } from "./run.ts"; vi.mock("@actions/github", () => ({ context: { repo: { owner: "changesets", repo: "action", }, ref: "refs/heads/some-branch", sha: "xeac7", }, getOctokit: () => ({ rest: mockedGithubMethods, graphql: mockedGraphql, }), })); vi.mock("@actions/core", { spy: true }); const mockedWarning = vi.mocked(core.warning); vi.mock("@changesets/ghcommit"); let mockedGithubMethods = { pulls: { create: vi.fn(), list: vi.fn(), }, repos: { createRelease: vi.fn(), }, }; let mockedGraphql = vi.fn(); const nodeModulesDir = path.join(import.meta.dirname, "..", "node_modules"); function createSimpleProjectFixture() { return createFixture({ node_modules: (api) => api.symlink(nodeModulesDir), ".changeset/config.json": JSON.stringify({}), "packages/pkg-a/package.json": JSON.stringify({ name: "changesets-dev-simple-project-pkg-a", version: "1.0.0", dependencies: { "changesets-dev-simple-project-pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "changesets-dev-simple-project-pkg-b", version: "1.0.0", }), "package.json": JSON.stringify({ name: "simple-project", version: "1.0.0", private: true, workspaces: ["packages/*"], }), "package-lock.json": "", }); } function createIgnoredPackageFixture() { return createFixture({ node_modules: (api) => api.symlink(nodeModulesDir), ".changeset/config.json": JSON.stringify({ ignore: ["changesets-dev-ignored-package-pkg-a"], }), "packages/pkg-a/package.json": JSON.stringify({ name: "changesets-dev-ignored-package-pkg-a", version: "1.0.0", dependencies: { "changesets-dev-ignored-package-pkg-b": "1.0.0", }, }), "packages/pkg-b/package.json": JSON.stringify({ name: "changesets-dev-ignored-package-pkg-b", version: "1.0.0", }), "package.json": JSON.stringify({ name: "ignored-package", version: "1.0.0", private: true, workspaces: ["packages/*"], }), "package-lock.json": "", }); } const writeChangesets = (changesets: Changeset[], cwd: string) => { return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd))); }; const createGithub = (cwd: string) => new GitHub({ cwd, githubToken: "@@GITHUB_TOKEN", pushWithGitCli: false, }); async function initializeGitRepository(cwd: string) { await exec("git", ["init"], { nodeOptions: { cwd }, throwOnError: true, }); } beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.unstubAllEnvs(); }); describe("publish", () => { it("warns when a custom publish script does not create the output file", async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; await initializeGitRepository(cwd); vi.stubEnv("RUNNER_TEMP", cwd); const result = await runPublish({ script: 'node -e "void 0"', github: createGithub(cwd), createGithubReleases: true, pushGitTags: true, cwd, }); expect(result).toEqual({ published: false, exitCode: 0 }); expect(mockedWarning).toHaveBeenCalledWith( expect.stringContaining( "GitHub releases and git tags cannot be created without this output", ), ); }); it("throws when the built-in publish command does not create the output file", async () => { await using fixture = await createFixture({ "node_modules/@changesets/cli/package.json": JSON.stringify({ name: "@changesets/cli", type: "module", }), "node_modules/@changesets/cli/bin.js": "", "package.json": JSON.stringify({ name: "simple-project", version: "1.0.0", }), "package-lock.json": "", }); const cwd = fixture.path; await initializeGitRepository(cwd); vi.stubEnv("RUNNER_TEMP", cwd); await expect( runPublish({ github: createGithub(cwd), createGithubReleases: true, pushGitTags: true, cwd, }), ).rejects.toThrow("Failed to read changesets output at"); }); }); describe("version", () => { it("creates simple PR", async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, { name: "changesets-dev-simple-project-pkg-b", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); }); it('creates a draft PR when prDraft is "create"', async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, prDraft: "create", }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); }); it("only includes bumped packages in the PR body", async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); }); it("doesn't include ignored package that got a dependency update in the PR body", async () => { await using fixture = await createIgnoredPackageFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-ignored-package-pkg-b", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); }); it("does not include changelog entries if full message exceeds size limit", async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: `# Non manus superum ## Nec cornibus aequa numinis multo onerosior adde Lorem markdownum undas consumpserat malas, nec est lupus; memorant gentisque ab limine auctore. Eatque et promptu deficit, quam videtur aequa est **faciat**, locus. Potentia deus habebat pia quam qui coniuge frater, tibi habent fertque viribus. E et cognoscere arcus, lacus aut sic pro crimina fuit tum **auxilium** dictis, qua, in. In modo. Nomen illa membra. > Corpora gratissima parens montibus tum coeperat qua remulus caelum Helenamque? > Non poenae modulatur Amathunta in concita superi, procerum pariter rapto cornu > munera. Perrhaebum parvo manus contingere, morari, spes per totiens ut > dividite proculcat facit, visa. Adspicit sequitur diffamatamque superi Phoebo qua quin lammina utque: per? Exit decus aut hac inpia, seducta mirantia extremo. Vidi pedes vetus. Saturnius fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. `, }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, prBodyMaxCharacters: 1000, }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); expect(mockedGithubMethods.pulls.create.mock.calls[0][0].body).toMatch( /The changelog information of each package has been omitted from this message/, ); }); it("does not include any release information if a message with simplified release info exceeds size limit", async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ data: { number: 123 }, })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: `# Non manus superum ## Nec cornibus aequa numinis multo onerosior adde Lorem markdownum undas consumpserat malas, nec est lupus; memorant gentisque ab limine auctore. Eatque et promptu deficit, quam videtur aequa est **faciat**, locus. Potentia deus habebat pia quam qui coniuge frater, tibi habent fertque viribus. E et cognoscere arcus, lacus aut sic pro crimina fuit tum **auxilium** dictis, qua, in. In modo. Nomen illa membra. > Corpora gratissima parens montibus tum coeperat qua remulus caelum Helenamque? > Non poenae modulatur Amathunta in concita superi, procerum pariter rapto cornu > munera. Perrhaebum parvo manus contingere, morari, spes per totiens ut > dividite proculcat facit, visa. Adspicit sequitur diffamatamque superi Phoebo qua quin lammina utque: per? Exit decus aut hac inpia, seducta mirantia extremo. Vidi pedes vetus. Saturnius fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. `, }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, prBodyMaxCharacters: 500, }); expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); expect(mockedGithubMethods.pulls.create.mock.calls[0][0].body).toMatch( /All release information have been omitted from this message, as the content exceeds the size limit/, ); }); it('updates an existing PR via GraphQL without converting it to draft when prDraft is "create"', async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [{ number: 123, node_id: "PR_kwDOA" }], })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, prDraft: "create", }); expect(mockedGraphql.mock.calls[0]).toMatchSnapshot(); }); it('updates an existing PR via GraphQL and converts it to draft when prDraft is "always"', async () => { await using fixture = await createSimpleProjectFixture(); const cwd = fixture.path; mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [{ number: 123, node_id: "PR_kwDOA" }], })); await writeChangesets( [ { releases: [ { name: "changesets-dev-simple-project-pkg-a", type: "minor", }, ], summary: "Awesome feature", }, ], cwd, ); await runVersion({ github: createGithub(cwd), cwd, prDraft: "always", }); expect(mockedGraphql.mock.calls[0]).toMatchSnapshot(); }); });