From 8a9c0813d46aadc3532b8cbe8f20757ca9c5a8ea Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:10:56 +0800 Subject: [PATCH 1/6] Update integration tests to use commitFilesFromBase64 --- tests/integration/core.test.ts | 408 +++++++++++++++++++++++++ tests/integration/env.ts | 29 -- tests/integration/globalSetup.ts | 14 - tests/integration/node.test.ts | 506 ------------------------------- tests/integration/util.ts | 50 --- tests/integration/utils.ts | 186 ++++++++++++ vitest.integration.config.ts | 18 +- 7 files changed, 602 insertions(+), 609 deletions(-) create mode 100644 tests/integration/core.test.ts delete mode 100644 tests/integration/env.ts delete mode 100644 tests/integration/globalSetup.ts delete mode 100644 tests/integration/node.test.ts delete mode 100644 tests/integration/util.ts create mode 100644 tests/integration/utils.ts diff --git a/tests/integration/core.test.ts b/tests/integration/core.test.ts new file mode 100644 index 0000000..c810a31 --- /dev/null +++ b/tests/integration/core.test.ts @@ -0,0 +1,408 @@ +import { exec } from "tinyexec"; +import { beforeAll, describe, expect, it, onTestFinished } from "vitest"; +import { commitFilesFromBase64 } from "../../src/core.ts"; +import { + createRefMutation, + getRepositoryMetadata, +} from "../../src/github/graphql/queries.ts"; +import type { CommitFilesFromBase64Args } from "../../src/interface.ts"; +import { + deleteBranch, + expectBranchDoesNotExist, + expectBranchHasFile, + expectBranchHasTree, + expectBranchNotHaveFile, + expectParentHasOid, + getOid, + getTempBranch, + octokit, + owner, + repo, + waitForGitHubToBeReady, +} from "./utils.ts"; + +// NOTE: These tests create and update actual branches in the repo. Add tests here sparingly and +// ensure the code doesn't affect the active repo branches. + +const BASIC_FILE_CHANGES_PATH = "foo.txt"; +const BASIC_FILE_BUFFER = Buffer.alloc(1024, "Hello, world!"); +const BASIC_FILE_CHANGES_OID = getOid(BASIC_FILE_BUFFER); +const BASIC_FILE_CONTENTS = BASIC_FILE_BUFFER.toString("base64"); +const BASIC_FILE_CHANGES = { + additions: [ + { + path: BASIC_FILE_CHANGES_PATH, + contents: BASIC_FILE_CONTENTS, + }, + ], +}; + +describe("commitFilesFromBase64", () => { + let repositoryId: string; + let testTargetCommit: string; + /** + * For tests, important that this commit is not an ancestor of TEST_TARGET_COMMIT, + * to ensure that non-fast-forward pushes are tested + */ + let testTargetCommit2: string; + let testTargetTree2: string; + + async function commitFilesFromBase64WithDefaults( + args: Omit< + CommitFilesFromBase64Args, + "octokit" | "owner" | "repo" | "message" | "base" + > & + Partial>, + ) { + return await commitFilesFromBase64({ + octokit, + owner, + repo, + message: "Test commit", + ...args, + // Allow overrides + base: args.base ?? { + commit: testTargetCommit, + }, + }); + } + + beforeAll(async () => { + const response = await getRepositoryMetadata(octokit, { + owner, + repo, + baseRef: "HEAD", + targetRef: "HEAD", + }); + if (!response?.id) { + throw new Error("Repository not found"); + } + repositoryId = response.id; + + // Get recent 2 commits to perform tests on + const result = await exec( + "git", + ["log", "-n", "2", "--pretty=format:%H %T"], + { + nodeOptions: { cwd: process.cwd() }, + }, + ); + const log = result.stdout + .trim() + .split("\n") + .map((line) => { + const [oid, tree] = line.split(" "); + return { oid, commit: { tree } }; + }); + + testTargetCommit = log[1]?.oid ?? "N/A"; + testTargetCommit2 = log[0]?.oid ?? "N/A"; + testTargetTree2 = log[0]?.commit.tree ?? "N/A"; + }); + + it("can commit files", async () => { + const branch = getTempBranch("basic-commit"); + onTestFinished(() => deleteBranch(branch)); + + const buffers = { + newFile: Buffer.from("Hello, world!"), + updated: Buffer.from("Hello, world!"), + nested: Buffer.from("Hello, world!"), + }; + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: { + additions: [ + { + path: "new-file.txt", + contents: buffers.newFile.toString("base64"), + }, + { + path: "README.md", + contents: buffers.updated.toString("base64"), + }, + { + path: "tests/file.txt", + contents: buffers.nested.toString("base64"), + }, + ], + deletions: [{ path: "CHANGELOG.md" }], + }, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: "new-file.txt", + fileOid: getOid(buffers.newFile), + }); + await expectBranchHasFile({ + branch, + filePath: "README.md", + fileOid: getOid(buffers.updated), + }); + await expectBranchHasFile({ + branch, + filePath: "tests/file.txt", + fileOid: getOid(buffers.nested), + }); + await expectBranchNotHaveFile({ branch, filePath: "CHANGELOG.md" }); + }); + + it("can commit large file sizes", async () => { + const branch = getTempBranch("file-size"); + onTestFinished(() => deleteBranch(branch)); + + const buffers = { + "1MiB": Buffer.alloc(1024 * 1024, "Hello, world!"), + "10MiB": Buffer.alloc(1024 * 1024 * 10, "Hello, world!"), + }; + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: { + additions: Object.entries(buffers).map(([sizeName, buffer]) => ({ + path: `${sizeName}.txt`, + contents: buffer.toString("base64"), + })), + }, + }); + + await waitForGitHubToBeReady(); + + for (const [sizeName, buffer] of Object.entries(buffers)) { + await expectBranchHasFile({ + branch, + filePath: `${sizeName}.txt`, + fileOid: getOid(buffer), + }); + } + }); + + it("can commit using branch as a base", async () => { + const branch = getTempBranch("branch-base"); + onTestFinished(() => deleteBranch(branch)); + + await commitFilesFromBase64WithDefaults({ + branch, + base: { + branch: "main", + }, + fileChanges: BASIC_FILE_CHANGES, + }); + + await waitForGitHubToBeReady(); + + // Don't test tree for this one as it will change over time / be unstable + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + }); + + it("can commit using tag as a base", async () => { + const branch = getTempBranch("tag-base"); + onTestFinished(() => deleteBranch(branch)); + + await commitFilesFromBase64WithDefaults({ + branch, + base: { + tag: "v1.4.0", + }, + fileChanges: BASIC_FILE_CHANGES, + }); + + await waitForGitHubToBeReady(); + + // Don't test tree for this one as it will change over time / be unstable + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + }); + + it("can commit using commit as a base", async () => { + const branch = getTempBranch("commit-base"); + onTestFinished(() => deleteBranch(branch)); + + await commitFilesFromBase64WithDefaults({ + branch, + base: { + commit: testTargetCommit, + }, + fileChanges: BASIC_FILE_CHANGES, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + }); + + describe("existing branches", () => { + it("can commit to existing branch when force is true", async () => { + const branch = getTempBranch("existing-branch-force"); + onTestFinished(() => deleteBranch(branch)); + + // Create an exiting branch + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: testTargetCommit2, + }, + }); + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: BASIC_FILE_CHANGES, + force: true, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + + await expectParentHasOid({ branch, oid: testTargetCommit }); + }); + + it("cleans up a pre-existing temporary branch when force is true", async () => { + const branch = getTempBranch("existing-branch-force-existing-temp"); + const tempBranch = getTempBranch(`temp-${branch}`); + onTestFinished(() => deleteBranch(branch)); + onTestFinished(() => deleteBranch(tempBranch)); + + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: testTargetCommit2, + }, + }); + + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${tempBranch}`, + oid: testTargetCommit2, + }, + }); + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: BASIC_FILE_CHANGES, + force: true, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + + await expectParentHasOid({ branch, oid: testTargetCommit }); + await expectBranchDoesNotExist(tempBranch); + }); + + it("cannot commit to existing branch when force is false", async () => { + const branch = getTempBranch("existing-branch-no-force"); + onTestFinished(() => deleteBranch(branch)); + + // Create an exiting branch + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: testTargetCommit2, + }, + }); + + await waitForGitHubToBeReady(); + + await expect(() => + commitFilesFromBase64WithDefaults({ + branch, + fileChanges: BASIC_FILE_CHANGES, + }), + ).rejects.toThrow( + `Branch ${branch} exists already and does not match base`, + ); + + await expectBranchHasTree({ + branch, + treeOid: testTargetTree2, + }); + }); + + it("can commit to existing branch when force is false and target matches base", async () => { + const branch = getTempBranch("existing-branch-matching-base"); + onTestFinished(() => deleteBranch(branch)); + + // Create an exiting branch + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: testTargetCommit, + }, + }); + + await waitForGitHubToBeReady(); + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: BASIC_FILE_CHANGES, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + }); + + it("can commit to same branch as base", async () => { + const branch = getTempBranch("same-branch-as-base"); + onTestFinished(() => deleteBranch(branch)); + + // Create an exiting branch + await createRefMutation(octokit, { + input: { + repositoryId, + name: `refs/heads/${branch}`, + oid: testTargetCommit, + }, + }); + + await waitForGitHubToBeReady(); + + await commitFilesFromBase64WithDefaults({ + branch, + fileChanges: BASIC_FILE_CHANGES, + }); + + await waitForGitHubToBeReady(); + + await expectBranchHasFile({ + branch, + filePath: BASIC_FILE_CHANGES_PATH, + fileOid: BASIC_FILE_CHANGES_OID, + }); + }); + }); +}); diff --git a/tests/integration/env.ts b/tests/integration/env.ts deleted file mode 100644 index 24c25c7..0000000 --- a/tests/integration/env.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { pino } from "pino"; - -export const ROOT_TEST_BRANCH_PREFIX = process.env.ROOT_TEST_BRANCH_PREFIX!; -export const ROOT_TEMP_DIRECTORY = process.env.ROOT_TEMP_DIRECTORY!; - -const GITHUB_TOKEN = process.env.GITHUB_TOKEN; -if (!GITHUB_TOKEN) { - throw new Error("GITHUB_TOKEN must be set"); -} - -const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY; - -const [owner, repo] = GITHUB_REPOSITORY?.split("/") || []; -if (!owner || !repo) { - throw new Error("GITHUB_REPOSITORY must be set"); -} - -export const ENV = { - GITHUB_TOKEN, -}; - -export const REPO = { owner, repo }; - -export const log = pino({ - level: process.env.RUNNER_DEBUG === "1" ? "debug" : "info", - transport: { - target: "pino-pretty", - }, -}); diff --git a/tests/integration/globalSetup.ts b/tests/integration/globalSetup.ts deleted file mode 100644 index 2c2a5a3..0000000 --- a/tests/integration/globalSetup.ts +++ /dev/null @@ -1,14 +0,0 @@ -import fs from "fs/promises"; - -export async function teardown() { - const directory = process.env.ROOT_TEMP_DIRECTORY; - if (!directory) { - throw new Error("ROOT_TEMP_DIRECTORY must be set"); - } - - console.log(`Deleting directory: ${directory}`); - - await fs.rm(directory, { force: true, recursive: true }).catch((error) => { - console.error(`Error deleting directory: ${error}`); - }); -} diff --git a/tests/integration/node.test.ts b/tests/integration/node.test.ts deleted file mode 100644 index dedd5c7..0000000 --- a/tests/integration/node.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { promises as fs } from "fs"; -import { getOctokit } from "@actions/github"; -import git from "isomorphic-git"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { - createRefMutation, - getRefTreeQuery, - getRepositoryMetadata, -} from "../../src/github/graphql/queries.ts"; -import { commitFilesFromBuffers } from "../../src/node.ts"; -import { ENV, REPO, ROOT_TEST_BRANCH_PREFIX, log } from "./env.ts"; -import { deleteBranches, waitForGitHubToBeReady } from "./util.ts"; - -// TODO: re-enable strict tree tests when GitHub have addressed the createRef -// bug that's currently used in integration tests -// See: https://github.com/orgs/community/discussions/136777 - -const octokit = getOctokit(ENV.GITHUB_TOKEN); - -const TEST_BRANCH_PREFIX = `${ROOT_TEST_BRANCH_PREFIX}-node`; - -// const TEST_TARGET_COMMIT = "fce2760017eab6d85388ed5cfdfac171559d80b3"; -/** - * For tests, important that this commit is not an ancestor of TEST_TARGET_COMMIT, - * to ensure that non-fast-forward pushes are tested - */ -// const TEST_TARGET_COMMIT_2 = "7ba8473f02849de3b5449b25fc83c5245d338d94"; -// const TEST_TARGET_TREE_2 = "95c9ea756f3686614dcdc1c42f7f654b684cdac2"; - -const BASIC_FILE_CHANGES_PATH = "foo.txt"; -const BASIC_FILE_CHANGES_OID = "0e23339619d605319ec4b49a0ac9dd94598eff8e"; -const BASIC_FILE_CONTENTS = { - message: { - headline: "Test commit", - body: "This is a test commit", - }, - fileChanges: { - additions: [ - { - path: BASIC_FILE_CHANGES_PATH, - contents: Buffer.alloc(1024, "Hello, world!"), - }, - ], - }, - log, -}; - -// const TEST_TARGET_TREE_WITH_BASIC_CHANGES = -// "a3431c9b42b71115c52bc6fbf9da3682cf0ed5e8"; - -const getTempBranchName = (branch: string) => - `changesets-ghcommit-temp/${branch}`; - -describe("node", () => { - const branches: string[] = []; - - // Set timeout to 1 minute - vi.setConfig({ testTimeout: 60 * 1000 }); - - let repositoryId: string; - - const expectBranchHasTree = async ({ - branch, - treeOid, - file, - }: { - branch: string; - treeOid?: string; - file?: { - path: string; - oid: string; - }; - }) => { - const ref = ( - await getRefTreeQuery(octokit, { - ...REPO, - ref: `refs/heads/${branch}`, - path: file?.path ?? "package.json", - }) - ).repository?.ref?.target; - - if (!ref) { - throw new Error("Unexpected missing ref"); - } - - if ("tree" in ref) { - if (treeOid) { - expect(ref.tree.oid).toEqual(treeOid); - } - if (file) { - expect(ref.file?.oid).toEqual(file.oid); - } - } else { - throw new Error("Expected ref to have a tree"); - } - }; - - const expectParentHasOid = async ({ - branch, - oid, - }: { - branch: string; - oid: string; - }) => { - const ref = ( - await getRefTreeQuery(octokit, { - ...REPO, - ref: `refs/heads/${branch}`, - path: "package.json", - }) - ).repository?.ref?.target; - - if (!ref || !("parents" in ref)) { - throw new Error("Unexpected result"); - } - - expect(ref.parents.nodes?.[0]?.oid).toEqual(oid); - }; - - const expectBranchDoesNotExist = async (branch: string) => { - await expect( - octokit.rest.git.getRef({ - ...REPO, - ref: `heads/${branch}`, - }), - ).rejects.toMatchObject({ - status: 404, - }); - }; - - let testTargetCommit: string; - /** - * For tests, important that this commit is not an ancestor of TEST_TARGET_COMMIT, - * to ensure that non-fast-forward pushes are tested - */ - let testTargetCommit2: string; - let testTargetTree2: string; - - beforeAll(async () => { - const response = await getRepositoryMetadata(octokit, { - ...REPO, - baseRef: "HEAD", - targetRef: "HEAD", - }); - if (!response?.id) { - throw new Error("Repository not found"); - } - repositoryId = response.id; - - // Get recent 2 commits to perform tests on - const log = await git.log({ fs, dir: process.cwd(), depth: 2 }); - testTargetCommit = log[1]?.oid ?? "N/A"; - testTargetCommit2 = log[0]?.oid ?? "N/A"; - testTargetTree2 = log[0]?.commit.tree ?? "N/A"; - }); - - describe("commitFilesFromBuffers", () => { - describe("can commit single file of various sizes", () => { - const SIZES_BYTES = { - "1KiB": { - sizeBytes: 1024, - treeOid: "547dfe4079b53c3b45a6717ac1ed6d98512f0a1c", - fileOid: "0e23339619d605319ec4b49a0ac9dd94598eff8e", - }, - "1MiB": { - sizeBytes: 1024 * 1024, - treeOid: "a6dca57388cf08de146bcc01a2113b218d6c2858", - fileOid: "a1d7fed1b4a8de1b665dc4f604015b2d87ef978f", - }, - "10MiB": { - sizeBytes: 1024 * 1024 * 10, - treeOid: "c4788256a2c1e3ea4267cff0502a656d992248ec", - fileOid: "e36e74edbb6d3fc181ef584a50f8ee55585d27cc", - }, - }; - - for (const [sizeName, { sizeBytes, fileOid }] of Object.entries( - SIZES_BYTES, - )) { - it(`Can commit a ${sizeName}`, async () => { - const branch = `${TEST_BRANCH_PREFIX}-${sizeName}`; - branches.push(branch); - const contents = Buffer.alloc(sizeBytes, "Hello, world!"); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - message: { - headline: "Test commit", - body: "This is a test commit", - }, - fileChanges: { - additions: [ - { - path: `${sizeName}.txt`, - contents, - }, - ], - }, - log, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - // TODO: re-enable - // treeOid, - file: { - path: `${sizeName}.txt`, - oid: fileOid, - }, - }); - }); - } - }); - - it("can commit using branch as a base", async () => { - const branch = `${TEST_BRANCH_PREFIX}-branch-base`; - branches.push(branch); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - branch: "main", - }, - ...BASIC_FILE_CONTENTS, - }); - - await waitForGitHubToBeReady(); - - // Don't test tree for this one as it will change over time / be unstable - await expectBranchHasTree({ - branch, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - }); - - it("can commit using tag as a base", async () => { - const branch = `${TEST_BRANCH_PREFIX}-tag-base`; - branches.push(branch); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - tag: "v1.4.0", - }, - ...BASIC_FILE_CONTENTS, - }); - - await waitForGitHubToBeReady(); - - // Don't test tree for this one as it will change over time / be unstable - await expectBranchHasTree({ - branch, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - }); - - it("can commit using commit as a base", async () => { - const branch = `${TEST_BRANCH_PREFIX}-commit-base`; - branches.push(branch); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - ...BASIC_FILE_CONTENTS, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - // TODO: re-enable - // treeOid: TEST_TARGET_TREE_WITH_BASIC_CHANGES, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - }); - - describe("existing branches", () => { - it("can commit to existing branch when force is true", async () => { - const branch = `${TEST_BRANCH_PREFIX}-existing-branch-force`; - branches.push(branch); - - // Create an exiting branch - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${branch}`, - oid: testTargetCommit2, - }, - }); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - ...BASIC_FILE_CONTENTS, - force: true, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - // TODO: re-enable - // treeOid: TEST_TARGET_TREE_WITH_BASIC_CHANGES, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - - await expectParentHasOid({ branch, oid: testTargetCommit }); - await expectBranchDoesNotExist(getTempBranchName(branch)); - }); - - it("cleans up a pre-existing temporary branch when force is true", async () => { - const branch = `${TEST_BRANCH_PREFIX}-existing-branch-force-existing-temp`; - const tempBranch = getTempBranchName(branch); - branches.push(branch, tempBranch); - - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${branch}`, - oid: testTargetCommit2, - }, - }); - - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${tempBranch}`, - oid: testTargetCommit2, - }, - }); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - ...BASIC_FILE_CONTENTS, - force: true, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - - await expectParentHasOid({ branch, oid: testTargetCommit }); - await expectBranchDoesNotExist(tempBranch); - }); - - it("cannot commit to existing branch when force is false", async () => { - const branch = `${TEST_BRANCH_PREFIX}-existing-branch-no-force`; - branches.push(branch); - - // Create an exiting branch - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${branch}`, - oid: testTargetCommit2, - }, - }); - - await waitForGitHubToBeReady(); - - await expect(() => - commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - ...BASIC_FILE_CONTENTS, - }), - ).rejects.toThrow( - `Branch ${branch} exists already and does not match base`, - ); - - await expectBranchHasTree({ - branch, - treeOid: testTargetTree2, - }); - }); - - it("can commit to existing branch when force is false and target matches base", async () => { - const branch = `${TEST_BRANCH_PREFIX}-existing-branch-matching-base`; - branches.push(branch); - - // Create an exiting branch - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${branch}`, - oid: testTargetCommit, - }, - }); - - await waitForGitHubToBeReady(); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - commit: testTargetCommit, - }, - ...BASIC_FILE_CONTENTS, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - // TODO: re-enable - // treeOid: TEST_TARGET_TREE_WITH_BASIC_CHANGES, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - }); - - it("can commit to same branch as base", async () => { - const branch = `${TEST_BRANCH_PREFIX}-same-branch-as-base`; - branches.push(branch); - - // Create an exiting branch - await createRefMutation(octokit, { - input: { - repositoryId, - name: `refs/heads/${branch}`, - oid: testTargetCommit, - }, - }); - - await waitForGitHubToBeReady(); - - await commitFilesFromBuffers({ - octokit, - ...REPO, - branch, - base: { - branch, - }, - ...BASIC_FILE_CONTENTS, - }); - - await waitForGitHubToBeReady(); - - await expectBranchHasTree({ - branch, - // TODO: re-enable - // treeOid: TEST_TARGET_TREE_WITH_BASIC_CHANGES, - file: { - path: BASIC_FILE_CHANGES_PATH, - oid: BASIC_FILE_CHANGES_OID, - }, - }); - }); - }); - }); - - afterAll(async () => { - console.info("Cleaning up test branches"); - - await deleteBranches(octokit, branches); - }); -}); diff --git a/tests/integration/util.ts b/tests/integration/util.ts deleted file mode 100644 index 4b7a52f..0000000 --- a/tests/integration/util.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - deleteRefMutation, - getRepositoryMetadata, - type GitHubClient, -} from "../../src/github/graphql/queries.js"; -import { REPO } from "./env.js"; - -export const deleteBranches = async ( - octokit: GitHubClient, - branches: string[], -) => - Promise.all( - branches.map(async (branch) => { - console.debug(`Deleting branch ${branch}`); - // Get Ref - const ref = await getRepositoryMetadata(octokit, { - ...REPO, - baseRef: `refs/heads/${branch}`, - targetRef: `refs/heads/${branch}`, - }); - - const refId = ref?.baseRef?.id; - - if (!refId) { - console.warn(`Branch ${branch} not found`); - return; - } - - await deleteRefMutation(octokit, { - input: { - refId, - }, - }); - - console.debug(`Deleted branch ${branch}`); - }), - ); - -/** - * GitHub sometimes has a delay between making changes to a git repo, - * and those changes being reflected in the API. - * - * This function is a workaround to wait for GitHub to be ready - * before running these assertions. - * - * It slows down testing a bit, - * but it's better than having flaky tests. - */ -export const waitForGitHubToBeReady = () => - new Promise((r) => setTimeout(r, 5000)); diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts new file mode 100644 index 0000000..bfe6f82 --- /dev/null +++ b/tests/integration/utils.ts @@ -0,0 +1,186 @@ +import crypto from "node:crypto"; +import { getOctokit } from "@actions/github"; +import { pino } from "pino"; +import { expect } from "vitest"; +import { + deleteRefMutation, + getRefTreeQuery, + getRepositoryMetadata, +} from "../../src/github/graphql/queries.ts"; + +export const githubToken = process.env.GITHUB_TOKEN!; +export const [owner, repo] = process.env.GITHUB_REPOSITORY!.split("/")!; + +export const octokit = getOctokit(githubToken); + +export const log = pino({ + level: process.env.RUNNER_DEBUG === "1" ? "debug" : "info", + transport: { + target: "pino-pretty", + }, +}); + +/** + * GitHub sometimes has a delay between making changes to a git repo, + * and those changes being reflected in the API. + * + * This function is a workaround to wait for GitHub to be ready + * before running these assertions. + * + * It slows down testing a bit, + * but it's better than having flaky tests. + */ +export async function waitForGitHubToBeReady() { + return await new Promise((r) => setTimeout(r, 5000)); +} + +const runId = crypto.randomBytes(4).toString("hex"); +export function getTempBranch(name: string) { + return `changesets-ghcommit-test-${runId}/${name}`; +} + +/** + * Calculate the SHA using git blob hash format + */ +export function getOid(contents: Buffer): string { + const header = Buffer.from(`blob ${contents.length}\0`); + return crypto + .createHash("sha1") + .update(header) + .update(contents) + .digest("hex"); +} + +// #region Assertion helpers + +export async function expectBranchHasTree({ + branch, + treeOid, +}: { + branch: string; + treeOid: string; +}) { + const ref = ( + await getRefTreeQuery(octokit, { + owner, + repo, + ref: `refs/heads/${branch}`, + path: "package.json", + }) + ).repository?.ref?.target; + + if (!ref) { + throw new Error("Unexpected missing ref"); + } + + expect(ref.tree.oid).toEqual(treeOid); +} + +export async function expectBranchHasFile({ + branch, + filePath, + fileOid, +}: { + branch: string; + filePath: string; + fileOid: string; +}) { + const ref = ( + await getRefTreeQuery(octokit, { + owner, + repo, + ref: `refs/heads/${branch}`, + path: filePath, + }) + ).repository?.ref?.target; + + if (!ref) { + throw new Error("Unexpected missing ref"); + } + + expect(ref.file?.oid).toEqual(fileOid); +} + +export async function expectBranchNotHaveFile({ + branch, + filePath, +}: { + branch: string; + filePath: string; +}) { + await expect(() => + getRefTreeQuery(octokit, { + owner, + repo, + ref: `refs/heads/${branch}`, + path: filePath, + }), + ).rejects.toThrow("Could not resolve file for path"); +} + +export async function expectParentHasOid({ + branch, + oid, +}: { + branch: string; + oid: string; +}) { + const ref = ( + await getRefTreeQuery(octokit, { + owner, + repo, + ref: `refs/heads/${branch}`, + path: "package.json", + }) + ).repository?.ref?.target; + + if (!ref || !("parents" in ref)) { + throw new Error("Unexpected result"); + } + + expect(ref.parents.nodes?.[0]?.oid).toEqual(oid); +} + +export async function expectBranchDoesNotExist(branch: string) { + await expect( + octokit.rest.git.getRef({ + owner, + repo, + ref: `heads/${branch}`, + }), + ).rejects.toMatchObject({ + status: 404, + }); +} + +// #endregion + +// #region Octokit helpers + +export async function deleteBranch(branch: string) { + console.debug(`Deleting branch ${branch}`); + // Get Ref + const ref = await getRepositoryMetadata(octokit, { + owner, + repo, + baseRef: `refs/heads/${branch}`, + targetRef: `refs/heads/${branch}`, + }); + + const refId = ref?.baseRef?.id; + + if (!refId) { + console.warn(`Branch ${branch} not found`); + return; + } + + await deleteRefMutation(octokit, { + input: { + refId, + }, + }); + + console.debug(`Deleted branch ${branch}`); +} + +// #endregion diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts index 2e28e6f..cfb9d77 100644 --- a/vitest.integration.config.ts +++ b/vitest.integration.config.ts @@ -1,6 +1,3 @@ -import { randomBytes } from "node:crypto"; -import os from "node:os"; -import path from "node:path"; import { loadEnvFile } from "node:process"; import { defineConfig } from "vitest/config"; @@ -8,17 +5,18 @@ try { loadEnvFile(); } catch {} -process.env.ROOT_TEST_BRANCH_PREFIX ??= `test-${randomBytes(4).toString("hex")}`; -process.env.ROOT_TEMP_DIRECTORY ??= path.join( - os.tmpdir(), - process.env.ROOT_TEST_BRANCH_PREFIX, -); +if (!process.env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN must be set"); +} +if (!process.env.GITHUB_REPOSITORY) { + throw new Error("GITHUB_REPOSITORY must be set"); +} export default defineConfig({ test: { experimental: { preParse: true }, clearMocks: true, - globalSetup: ["./tests/integration/globalSetup.ts"], - include: ["tests/integration/**/*.test.ts"], + testTimeout: 60_000, + include: ["tests/integration/*.test.ts"], }, }); From 4120cdd47f8a1080ebf70f0dd33258f36af47a0e Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:17:44 +0800 Subject: [PATCH 2/6] Use GITHUB_RUN_ID in branch name --- tests/integration/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index bfe6f82..bf14488 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -34,9 +34,10 @@ export async function waitForGitHubToBeReady() { return await new Promise((r) => setTimeout(r, 5000)); } -const runId = crypto.randomBytes(4).toString("hex"); +const runHash = crypto.randomBytes(4).toString("hex"); +const runId = process.env.GITHUB_RUN_ID ?? "local"; export function getTempBranch(name: string) { - return `changesets-ghcommit-test-${runId}/${name}`; + return `changesets-ghcommit-test-${runHash}-id-${runId}/${name}`; } /** From 1413aa350665cfc1a9f0364f534dc13cfb4a56bb Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:20:45 +0800 Subject: [PATCH 3/6] Rename --- tests/integration/core.test.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/integration/core.test.ts b/tests/integration/core.test.ts index c810a31..6acd06b 100644 --- a/tests/integration/core.test.ts +++ b/tests/integration/core.test.ts @@ -80,24 +80,22 @@ describe("commitFilesFromBase64", () => { repositoryId = response.id; // Get recent 2 commits to perform tests on - const result = await exec( + const logOutput = await exec( "git", ["log", "-n", "2", "--pretty=format:%H %T"], - { - nodeOptions: { cwd: process.cwd() }, - }, + { nodeOptions: { cwd: process.cwd() } }, ); - const log = result.stdout + const logs = logOutput.stdout .trim() .split("\n") .map((line) => { const [oid, tree] = line.split(" "); - return { oid, commit: { tree } }; + return { oid, tree }; }); - testTargetCommit = log[1]?.oid ?? "N/A"; - testTargetCommit2 = log[0]?.oid ?? "N/A"; - testTargetTree2 = log[0]?.commit.tree ?? "N/A"; + testTargetCommit = logs[1]?.oid ?? "N/A"; + testTargetCommit2 = logs[0]?.oid ?? "N/A"; + testTargetTree2 = logs[0]?.tree ?? "N/A"; }); it("can commit files", async () => { From a2fda08a92e50df475469373af59e9adc034d51c Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:41:43 +0800 Subject: [PATCH 4/6] Fix tests --- tests/integration/core.test.ts | 16 +++++++++--- tests/integration/utils.ts | 47 +++++++++++++++++----------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/tests/integration/core.test.ts b/tests/integration/core.test.ts index 6acd06b..260ae4b 100644 --- a/tests/integration/core.test.ts +++ b/tests/integration/core.test.ts @@ -37,6 +37,11 @@ const BASIC_FILE_CHANGES = { ], }; +// Match branch name as in core.ts +function getInternalTempBranch(name: string) { + return `changesets-ghcommit-test/${name}`; +} + describe("commitFilesFromBase64", () => { let repositoryId: string; let testTargetCommit: string; @@ -247,7 +252,9 @@ describe("commitFilesFromBase64", () => { describe("existing branches", () => { it("can commit to existing branch when force is true", async () => { const branch = getTempBranch("existing-branch-force"); + const internalTempBranch = getInternalTempBranch(branch); onTestFinished(() => deleteBranch(branch)); + onTestFinished(() => deleteBranch(internalTempBranch, true)); // Create an exiting branch await createRefMutation(octokit, { @@ -273,13 +280,14 @@ describe("commitFilesFromBase64", () => { }); await expectParentHasOid({ branch, oid: testTargetCommit }); + await expectBranchDoesNotExist(internalTempBranch); }); it("cleans up a pre-existing temporary branch when force is true", async () => { const branch = getTempBranch("existing-branch-force-existing-temp"); - const tempBranch = getTempBranch(`temp-${branch}`); + const internalTempBranch = getInternalTempBranch(branch); onTestFinished(() => deleteBranch(branch)); - onTestFinished(() => deleteBranch(tempBranch)); + onTestFinished(() => deleteBranch(internalTempBranch, true)); await createRefMutation(octokit, { input: { @@ -292,7 +300,7 @@ describe("commitFilesFromBase64", () => { await createRefMutation(octokit, { input: { repositoryId, - name: `refs/heads/${tempBranch}`, + name: `refs/heads/${internalTempBranch}`, oid: testTargetCommit2, }, }); @@ -312,7 +320,7 @@ describe("commitFilesFromBase64", () => { }); await expectParentHasOid({ branch, oid: testTargetCommit }); - await expectBranchDoesNotExist(tempBranch); + await expectBranchDoesNotExist(internalTempBranch); }); it("cannot commit to existing branch when force is false", async () => { diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index bf14488..bf24f44 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -158,30 +158,31 @@ export async function expectBranchDoesNotExist(branch: string) { // #region Octokit helpers -export async function deleteBranch(branch: string) { - console.debug(`Deleting branch ${branch}`); - // Get Ref - const ref = await getRepositoryMetadata(octokit, { - owner, - repo, - baseRef: `refs/heads/${branch}`, - targetRef: `refs/heads/${branch}`, - }); - - const refId = ref?.baseRef?.id; - - if (!refId) { - console.warn(`Branch ${branch} not found`); - return; +export async function deleteBranch(branch: string, allowNotExist = false) { + try { + const ref = await getRepositoryMetadata(octokit, { + owner, + repo, + baseRef: `refs/heads/${branch}`, + targetRef: `refs/heads/${branch}`, + }); + + const refId = ref?.baseRef?.id; + if (!refId) { + if (!allowNotExist) { + console.warn(`Branch ${branch} not found`); + } + return; + } + + await deleteRefMutation(octokit, { + input: { + refId, + }, + }); + } catch (error) { + console.error(`Failed to delete branch ${branch}:`, error); } - - await deleteRefMutation(octokit, { - input: { - refId, - }, - }); - - console.debug(`Deleted branch ${branch}`); } // #endregion From 443db4966e33896751d106362e08cc042d9816e1 Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:45:27 +0800 Subject: [PATCH 5/6] Ugh typo --- tests/integration/core.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/core.test.ts b/tests/integration/core.test.ts index 260ae4b..2fd7fef 100644 --- a/tests/integration/core.test.ts +++ b/tests/integration/core.test.ts @@ -39,7 +39,7 @@ const BASIC_FILE_CHANGES = { // Match branch name as in core.ts function getInternalTempBranch(name: string) { - return `changesets-ghcommit-test/${name}`; + return `changesets-ghcommit-temp/${name}`; } describe("commitFilesFromBase64", () => { From f8392cfbcbe420e8c52ef33b42e636eefc91bf17 Mon Sep 17 00:00:00 2001 From: bluwy Date: Sat, 13 Jun 2026 01:50:25 +0800 Subject: [PATCH 6/6] Add missing test --- tests/integration/core.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/core.test.ts b/tests/integration/core.test.ts index 2fd7fef..0b9096f 100644 --- a/tests/integration/core.test.ts +++ b/tests/integration/core.test.ts @@ -45,6 +45,7 @@ function getInternalTempBranch(name: string) { describe("commitFilesFromBase64", () => { let repositoryId: string; let testTargetCommit: string; + /** * For tests, important that this commit is not an ancestor of TEST_TARGET_COMMIT, * to ensure that non-fast-forward pushes are tested @@ -159,6 +160,7 @@ describe("commitFilesFromBase64", () => { onTestFinished(() => deleteBranch(branch)); const buffers = { + "1KiB": Buffer.alloc(1024, "Hello, world!"), "1MiB": Buffer.alloc(1024 * 1024, "Hello, world!"), "10MiB": Buffer.alloc(1024 * 1024 * 10, "Hello, world!"), };