diff --git a/.gitignore b/.gitignore index 7ebb6ce..b564c26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules .env.development.local .env.test.local .env.production.local +.envrc # Testing coverage diff --git a/README.md b/README.md index 81d2725..3391c63 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![View on NPM](https://badgen.net/npm/v/@changesets/ghcommit)](https://www.npmjs.com/package/@changesets/ghcommit) -NPM / TypeScript package to commit changes GitHub repositories using the GraphQL API. +NPM / TypeScript package to commit changes to GitHub repositories using the GitHub API. ## Why? @@ -37,7 +37,7 @@ pnpm install @changesets/ghcommit ### Usage in github actions -All functions in this library that interact with the GitHub API require an octokit client that can execute GraphQL. If you are writing code that is designed to be run from within a GitHub Action, this can be done using the `@actions.github` library: +All functions in this library that interact with the GitHub API require an octokit client that can execute both GraphQL queries and REST API requests. If you are writing code that is designed to be run from within a GitHub Action, this can be done using the `@actions/github` library: ```ts import { getOctokit } from "@actions/github"; @@ -45,6 +45,14 @@ import { getOctokit } from "@actions/github"; const octokit = getOctokit(process.env.GITHUB_TOKEN); ``` +Alternatively, you can use `@octokit/core` directly: + +```ts +import { Octokit } from "@octokit/core"; + +const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); +``` + ### Importing specific modules To allow for you to produce smaller bundle sizes, the functionality exposed in this package is grouped into specific modules that only import the packages required for their use. We recommend that you import from the specific modules rather than the root of the package. @@ -253,6 +261,11 @@ In addition to `CommitFilesBasedArgs`, this function has the following arguments additions?: Array<{ path: string; contents: Buffer; + /** + * Optional file mode. Defaults to '100644' (normal file). + * Can be any valid git file mode string (e.g., '100755' for executable). + */ + mode?: string; }>; deletions?: string[]; }; @@ -264,6 +277,7 @@ Example: ```ts import { context, getOctokit } from "@actions/github"; import { commitFilesFromBuffers } from "@changesets/ghcommit/node"; +import { FileModes } from "@changesets/ghcommit"; const octokit = getOctokit(process.env.GITHUB_TOKEN); @@ -282,6 +296,11 @@ await commitFilesFromBuffers({ path: "hello/world.txt", contents: Buffer.alloc(1024, "Hello, world!"), }, + { + path: "scripts/run.sh", + contents: Buffer.from("#!/bin/bash\necho 'Hello!'"), + mode: FileModes.executableFile, // '100755' + }, ], }, }); @@ -290,12 +309,38 @@ await commitFilesFromBuffers({ ## Known Limitations Due to using the GitHub API to make changes to repository contents, -there are some things it's not possible to commit, -and where using the Git CLI is still required. +there are some things that may not work as expected: + +- Submodule changes (gitlinks with mode `160000`) - while the mode is supported, submodule-specific behavior may not work correctly + +### File Mode Support -- Executable files -- Symbolic Links -- Submodule changes +This library supports any git file mode. Common modes include: + +- `100644` - Normal file (default) +- `100755` - Executable file +- `120000` - Symbolic link +- `040000` - Directory (subdirectory) +- `160000` - Submodule (gitlink) + +**Automatic detection:** + +- When using `commitChangesFromRepo`, file modes are automatically detected from the git working directory. +- When using `commitFilesFromDirectory`, file modes are automatically detected from the filesystem (based on execute bits). +- When using `commitFilesFromBuffers`, you can explicitly specify any file mode string. + +```ts +import { FileModes } from "@changesets/ghcommit"; + +// Convenience constants: +FileModes.file; // '100644' - Normal file (default) +FileModes.executableFile; // '100755' - Executable file +FileModes.symlink; // '120000' - Symbolic link + +// Or use any git mode string directly: +{ path: "script.sh", contents: buffer, mode: "100755" } +{ path: "custom", contents: buffer, mode: "100644" } +``` ## Other Tools / Alternatives diff --git a/package.json b/package.json index 1b754fe..9639965 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "access": "public" }, "dependencies": { - "isomorphic-git": "^1.27.1" + "isomorphic-git": "^1.27.1", + "queue": "^6.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 647a8bc..f26e7ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: isomorphic-git: specifier: ^1.27.1 version: 1.27.1 + queue: + specifier: ^6.0.0 + version: 6.0.2 devDependencies: '@actions/github': specifier: ^6.0.0 @@ -3127,6 +3130,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -7570,6 +7576,10 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} react-is@18.3.1: {} diff --git a/src/core.ts b/src/core.ts index ffc640a..1f3e64e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,19 +1,38 @@ import { - createCommitOnBranchQuery, createRefMutation, getRepositoryMetadata, updateRefMutation, } from "./github/graphql/queries.js"; -import type { - CreateCommitOnBranchMutationVariables, - GetRepositoryMetadataQuery, -} from "./github/graphql/generated/operations.js"; +import type { GetRepositoryMetadataQuery } from "./github/graphql/generated/operations.js"; import { CommitFilesFromBase64Args, CommitFilesResult, GitBase, + FileModes, } from "./interface.js"; import { CommitMessage } from "./github/graphql/generated/types.js"; +import { isUtf8 } from "buffer"; +import Queue from "queue"; + +// Types for GitHub Git Data API responses +interface GitCommitResponse { + sha: string; + tree: { + sha: string; + }; +} + +interface GitBlobResponse { + sha: string; +} + +interface GitTreeResponse { + sha: string; +} + +interface GitNewCommitResponse { + sha: string; +} const getBaseRef = (base: GitBase): string => { if ("branch" in base) { @@ -164,21 +183,157 @@ export const commitFilesFromBase64 = async ({ } : message; - await log?.debug(`Creating commit on branch ${branch}`); - const createCommitMutation: CreateCommitOnBranchMutationVariables = { - input: { - branch: { - id: refId, - }, - expectedHeadOid: baseOid, - message: finalMessage, - fileChanges, + const commitMessageStr = finalMessage.body + ? `${finalMessage.headline}\n\n${finalMessage.body}` + : finalMessage.headline; + + await log?.debug(`Creating commit on branch ${branch} using Git Data API`); + + // Check if the octokit instance supports REST API calls + if (!octokit.request) { + throw new Error( + "The provided octokit instance does not support REST API calls (missing request method). " + + "Please provide an Octokit instance that supports both GraphQL and REST API, such as @octokit/core.", + ); + } + + // Step 1: Get the base tree from the parent commit + log?.debug(`Getting base commit ${baseOid}`); + const baseCommit = await octokit.request( + "GET /repos/{owner}/{repo}/git/commits/{commit_sha}", + { + owner, + repo, + commit_sha: baseOid, }, - }; - log?.debug(JSON.stringify(createCommitMutation, null, 2)); + ); + const baseTreeSha = baseCommit.data.tree.sha; + log?.debug(`Base tree SHA: ${baseTreeSha}`); + + // Step 2: Create blobs for each file addition + const treeItems: Array< + { + path: string; + mode: string; + type: "blob" | "tree" | "commit"; + } & ( + | { + sha: string | null; + } + | { + content: string; + } + ) + > = []; + + // Add file additions + if (fileChanges.additions) { + // Use a queue as we might have to upload a bunch of blobs concurrently + const additionsProcessor = new Queue({ + concurrency: 5, + }); + additionsProcessor.push( + ...fileChanges.additions.map((addition) => { + return async () => { + if (isUtf8(Buffer.from(addition.contents, "base64"))) { + log?.debug(`Using utf8 content directly for ${addition.path}`); + + treeItems.push({ + path: addition.path, + mode: addition.mode || FileModes.file, + type: "blob", + content: Buffer.from(addition.contents, "base64").toString( + "utf-8", + ), + }); + } else { + log?.debug(`Creating blob for non-utf8 file at ${addition.path}`); + const blobResponse = await octokit.request!( + "POST /repos/{owner}/{repo}/git/blobs", + { + owner, + repo, + content: addition.contents, + encoding: "base64", + }, + ); + + const mode = addition.mode || FileModes.file; + log?.debug( + `Created blob ${blobResponse.data.sha} for ${addition.path} with mode ${mode}`, + ); + + treeItems.push({ + path: addition.path, + mode: mode, + type: "blob", + sha: blobResponse.data.sha, + }); + } + }; + }), + ); + await new Promise((resolve, reject) => additionsProcessor.start((err) => { + if (err) { + reject(err) + } else { + resolve(); + } + })); + } + + // Add file deletions (set sha to null) + if (fileChanges.deletions) { + for (const deletion of fileChanges.deletions) { + log?.debug(`Marking ${deletion.path} for deletion`); + treeItems.push({ + path: deletion.path, + mode: "100644", + type: "blob", + sha: null, + }); + } + } + + // Step 3: Create new tree with the changes + log?.debug(`Creating tree with ${treeItems.length} items`); + const newTree = await octokit.request( + "POST /repos/{owner}/{repo}/git/trees", + { + owner, + repo, + base_tree: baseTreeSha, + tree: treeItems, + }, + ); + log?.debug(`Created tree ${newTree.data.sha}`); + + // Step 4: Create the commit + log?.debug(`Creating commit with message: ${finalMessage.headline}`); + const newCommit = await octokit.request( + "POST /repos/{owner}/{repo}/git/commits", + { + owner, + repo, + message: commitMessageStr, + tree: newTree.data.sha, + parents: [baseOid], + }, + ); + log?.debug(`Created commit ${newCommit.data.sha}`); + + // Step 5: Update the branch ref to point to the new commit + log?.debug(`Updating ref ${targetRef} to ${newCommit.data.sha}`); + await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", { + owner, + repo, + ref: `heads/${branch}`, + sha: newCommit.data.sha, + force: false, + }); + log?.debug(`Updated ref successfully`); - const result = await createCommitOnBranchQuery(octokit, createCommitMutation); return { - refId: result.createCommitOnBranch?.ref?.id ?? null, + refId: refId, }; }; diff --git a/src/fs.ts b/src/fs.ts index d2d633e..fd60593 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,22 +1,77 @@ import { promises as fs } from "fs"; import * as path from "path"; -import type { FileAddition } from "./github/graphql/generated/types.js"; import { commitFilesFromBuffers } from "./node.js"; import { CommitFilesFromDirectoryArgs, CommitFilesResult, } from "./interface.js"; +// File type constants from Node.js fs.constants +const S_IFMT = 0o170000; // File type mask +const S_IFREG = 0o100000; // Regular file +const S_IFDIR = 0o040000; // Directory +const S_IFLNK = 0o120000; // Symbolic link + +/** + * Convert filesystem mode to git mode string + * Maps Unix file mode to git's internal mode representation + */ +const getFileModeFromStats = (mode: number): string => { + const fileType = mode & S_IFMT; + + if (fileType === S_IFLNK) { + // Symbolic link + return "120000"; + } + + if (fileType === S_IFDIR) { + // Directory + return "040000"; + } + + if (fileType === S_IFREG) { + // Regular file - check if executable + const isExecutable = (mode & 0o111) !== 0; + return isExecutable ? "100755" : "100644"; + } + + // Default to regular file for unknown types + return "100644"; +}; + export const commitFilesFromDirectory = async ({ cwd, fileChanges, ...otherArgs }: CommitFilesFromDirectoryArgs): Promise => { - const additions: FileAddition[] = await Promise.all( - (fileChanges.additions || []).map(async (p) => { + const additions = await Promise.all( + (fileChanges.additions || []).map(async (item) => { + // Handle both string paths and objects with path and optional mode + const filePath = typeof item === "string" ? item : item.path; + const explicitMode = typeof item === "object" ? item.mode : undefined; + + const fullPath = path.join(cwd, filePath); + // Use lstat to not follow symlinks + const stats = explicitMode ? null : await fs.lstat(fullPath); + + // Determine mode + const mode = + explicitMode || (stats ? getFileModeFromStats(stats.mode) : "100644"); + + // For symlinks, read the link target; otherwise read file contents + let contents: Buffer; + if (stats?.isSymbolicLink()) { + // For symlinks, the content is the target path + const linkTarget = await fs.readlink(fullPath); + contents = Buffer.from(linkTarget, "utf-8"); + } else { + contents = await fs.readFile(fullPath); + } + return { - path: p, - contents: await fs.readFile(path.join(cwd, p)), + path: filePath, + contents, + mode, }; }), ); diff --git a/src/git.ts b/src/git.ts index a6e2a80..4b2a2fe 100644 --- a/src/git.ts +++ b/src/git.ts @@ -11,13 +11,23 @@ import { relative, resolve } from "path"; /** * @see https://isomorphic-git.org/docs/en/walk#walkerentry-mode */ -const FILE_MODES = { +const GIT_FILE_MODES = { directory: 0o40000, file: 0o100644, executableFile: 0o100755, symlink: 0o120000, } as const; +/** + * Convert git numeric mode to string mode for GitHub API + * Returns the octal representation as a string (e.g., "100644", "100755") + */ +const convertGitModeToApiMode = (mode: number): string => { + // Convert the numeric mode to its octal string representation + // Git modes are stored as octal numbers, so we convert to octal string + return mode.toString(8); +}; + export const commitChangesFromRepo = async ({ base, cwd: workingDirectory, @@ -74,23 +84,39 @@ export const commitChangesFromRepo = async ({ ) { return null; } - if ( - (await commit?.mode()) === FILE_MODES.symlink || - (await workdir?.mode()) === FILE_MODES.symlink - ) { - throw new Error( - `Unexpected symlink at ${filepath}, GitHub API only supports files and directories. You may need to add this file to .gitignore`, - ); + + // Handle symlinks specially - oid() can fail for broken symlinks + const workdirMode = await workdir?.mode(); + const commitMode = await commit?.mode(); + const isWorkdirSymlink = workdirMode === GIT_FILE_MODES.symlink; + const isCommitSymlink = commitMode === GIT_FILE_MODES.symlink; + + let prevOid: string | undefined; + let currentOid: string | undefined; + + // For symlinks, compute oid from the link target path + if (isCommitSymlink) { + prevOid = await commit?.oid().catch(() => undefined); + } else { + prevOid = await commit?.oid(); } - if ((await workdir?.mode()) === FILE_MODES.executableFile) { - throw new Error( - `Unexpected executable file at ${filepath}, GitHub API only supports non-executable files and directories. You may need to add this file to .gitignore`, - ); + + if (isWorkdirSymlink) { + // For symlinks, we need to compute the oid ourselves since isomorphic-git + // can fail for broken symlinks. We'll skip the oid check and always include the file. + currentOid = undefined; + } else { + currentOid = await workdir?.oid(); } - const prevOid = await commit?.oid(); - const currentOid = await workdir?.oid(); + // Don't include files that haven't changed, and exist in both trees - if (prevOid === currentOid && !commit === !workdir) { + // Skip this check for symlinks since oid computation can fail + if ( + !isWorkdirSymlink && + !isCommitSymlink && + prevOid === currentOid && + !commit === !workdir + ) { return null; } // Iterate through anything that may be a directory in either the @@ -119,13 +145,40 @@ export const commitChangesFromRepo = async ({ return null; } else { // File was added / updated - const arr = await workdir.content(); - if (!arr) { - throw new Error(`Could not determine content of file ${filepath}`); + const fileMode = await workdir.mode(); + const isSymlink = fileMode === GIT_FILE_MODES.symlink; + + let contents: Buffer; + if (isSymlink) { + // For symlinks, read the link target path using the filesystem + // isomorphic-git's content() returns null for symlinks + const symlinkPath = resolve(repoRoot, filepath); + const linkTarget = await fs.readlink(symlinkPath); + + // Check if symlink target exists (resolve relative to symlink's directory) + const { dirname } = await import("path"); + const targetPath = resolve(dirname(symlinkPath), linkTarget); + try { + await fs.access(targetPath); + } catch { + throw new Error( + `Broken symlink detected: ${filepath} points to non-existent target ${linkTarget}`, + ); + } + + contents = Buffer.from(linkTarget, "utf-8"); + } else { + const arr = await workdir.content(); + if (!arr) { + throw new Error(`Could not determine content of file ${filepath}`); + } + contents = Buffer.from(arr); } + additions.push({ path: filepath, - contents: Buffer.from(arr), + contents, + mode: convertGitModeToApiMode(fileMode ?? GIT_FILE_MODES.file), }); } return true; diff --git a/src/github/graphql/queries.ts b/src/github/graphql/queries.ts index 6665af2..ffd9667 100644 --- a/src/github/graphql/queries.ts +++ b/src/github/graphql/queries.ts @@ -1,5 +1,6 @@ export type GitHubClient = { graphql: (query: string, variables: any) => Promise; + request?: (route: string, options?: any) => Promise<{ data: T }>; }; import type { diff --git a/src/index.ts b/src/index.ts index 41cdeb7..68ab968 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,6 @@ export * as queries from "./github/graphql/queries.js"; export { commitFilesFromBase64 } from "./core.js"; export { commitChangesFromRepo } from "./git.js"; export { commitFilesFromDirectory } from "./fs.js"; +export { commitFilesFromBuffers } from "./node.js"; +export { FileModes } from "./interface.js"; +export type { FileMode, FileChangesWithModes } from "./interface.js"; diff --git a/src/interface.ts b/src/interface.ts index 34f4c18..e28c88c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,7 +1,4 @@ -import type { - CommitMessage, - FileChanges, -} from "./github/graphql/generated/types.js"; +import type { CommitMessage } from "./github/graphql/generated/types.js"; import type { GitHubClient } from "./github/graphql/queries.js"; import type { Logger } from "./logging.js"; @@ -45,11 +42,42 @@ export interface CommitFilesSharedArgsWithBase extends CommitFilesBasedArgs { base: GitBase; } +/** + * File changes with support for file modes. + * This is used instead of the GraphQL FileChanges type to support file modes. + */ +export interface FileChangesWithModes { + additions?: Array<{ + path: string; + contents: string; // base64 encoded + /** + * The file mode. Defaults to '100644' (normal file). + * Use '100755' for executable files. + * Can be any valid git file mode string. + */ + mode?: string; + }>; + deletions?: Array<{ + path: string; + }>; +} + export interface CommitFilesFromBase64Args extends CommitFilesSharedArgsWithBase { - fileChanges: FileChanges; + fileChanges: FileChangesWithModes; } +/** + * Git file modes + */ +export const FileModes = { + file: "100644", + executableFile: "100755", + symlink: "120000", +} as const; + +export type FileMode = (typeof FileModes)[keyof typeof FileModes]; + export interface CommitFilesFromBuffersArgs extends CommitFilesSharedArgsWithBase { /** @@ -59,6 +87,12 @@ export interface CommitFilesFromBuffersArgs additions?: Array<{ path: string; contents: Buffer; + /** + * The file mode. Defaults to '100644' (normal file). + * Use '100755' for executable files. + * Can be any valid git file mode string. + */ + mode?: string; }>; deletions?: string[]; }; @@ -76,8 +110,11 @@ export interface CommitFilesFromDirectoryArgs * to add or delete from the branch on GitHub. */ fileChanges: { - /** File paths, relative to {@link cwd}, to remove from the repo. */ - additions?: string[]; + /** + * File paths, relative to {@link cwd}, to add to the repo. + * Can be strings (file paths) or objects with path and mode. + */ + additions?: Array; /** File paths, relative to the repository root, to remove from the repo. */ deletions?: string[]; }; diff --git a/src/node.ts b/src/node.ts index 565d0ea..d93d0a0 100644 --- a/src/node.ts +++ b/src/node.ts @@ -8,9 +8,10 @@ export const commitFilesFromBuffers = async ({ return commitFilesFromBase64({ ...otherArgs, fileChanges: { - additions: fileChanges.additions?.map(({ path, contents }) => ({ + additions: fileChanges.additions?.map(({ path, contents, mode }) => ({ path, contents: contents.toString("base64"), + mode, })), deletions: fileChanges.deletions?.map((path) => ({ path })), }, diff --git a/src/test/integration/git.test.ts b/src/test/integration/git.test.ts index 2e46c60..7edfc82 100644 --- a/src/test/integration/git.test.ts +++ b/src/test/integration/git.test.ts @@ -324,8 +324,8 @@ describe("git", () => { }); } - describe(`should throw appropriate error when symlink is present`, () => { - it(`and file does not exist`, async () => { + describe(`should support symlinks`, () => { + it(`should throw error when symlink target does not exist`, async () => { const branch = `${TEST_BRANCH_PREFIX}-invalid-symlink-error`; branches.push(branch); @@ -352,8 +352,8 @@ describe("git", () => { await makeFileChanges(repoDirectory, "with-included-invalid-symlink"); - // Push the changes - await expect(() => + // Push the changes - broken symlinks should throw an error + await expect( commitChangesFromRepo({ octokit, ...REPO, @@ -365,12 +365,10 @@ describe("git", () => { cwd: repoDirectory, log, }), - ).rejects.toThrow( - "Unexpected symlink at some-dir/nested, GitHub API only supports files and directories. You may need to add this file to .gitignore", - ); + ).rejects.toThrow(/Broken symlink detected/); }); - it(`and file exists`, async () => { + it(`when symlink target exists`, async () => { const branch = `${TEST_BRANCH_PREFIX}-valid-symlink-error`; branches.push(branch); @@ -397,26 +395,40 @@ describe("git", () => { await makeFileChanges(repoDirectory, "with-included-valid-symlink"); - // Push the changes - await expect(() => - commitChangesFromRepo({ - octokit, + // Push the changes - symlinks are now supported + const result = await commitChangesFromRepo({ + octokit, + ...REPO, + branch, + message: { + headline: "Test commit", + body: "This is a test commit", + }, + cwd: repoDirectory, + log, + }); + + expect(result.refId).toBeTruthy(); + + await waitForGitHubToBeReady(); + + // Verify the commit was created successfully with the symlink + const ref = ( + await getRefTreeQuery(octokit, { ...REPO, - branch, - message: { - headline: "Test commit", - body: "This is a test commit", - }, - cwd: repoDirectory, - log, - }), - ).rejects.toThrow( - "Unexpected symlink at some-dir/nested, GitHub API only supports files and directories. You may need to add this file to .gitignore", - ); + ref: `refs/heads/${branch}`, + path: "new-file.txt", + }) + ).repository?.ref?.target; + + expect(ref).toBeTruthy(); + if (ref && "file" in ref) { + expect(ref.file?.oid).toBeTruthy(); + } }); }); - it(`should throw appropriate error when executable file is present`, async () => { + it(`should support executable files`, async () => { const branch = `${TEST_BRANCH_PREFIX}-executable-file`; branches.push(branch); @@ -443,22 +455,36 @@ describe("git", () => { await makeFileChanges(repoDirectory, "with-executable-file"); - // Push the changes - await expect(() => - commitChangesFromRepo({ - octokit, + // Push the changes - executable files are now supported + const result = await commitChangesFromRepo({ + octokit, + ...REPO, + branch, + message: { + headline: "Test commit", + body: "This is a test commit", + }, + cwd: repoDirectory, + log, + }); + + expect(result.refId).toBeTruthy(); + + await waitForGitHubToBeReady(); + + // Verify the commit was created successfully with the executable file + const ref = ( + await getRefTreeQuery(octokit, { ...REPO, - branch, - message: { - headline: "Test commit", - body: "This is a test commit", - }, - cwd: repoDirectory, - log, - }), - ).rejects.toThrow( - "Unexpected executable file at executable-file.sh, GitHub API only supports non-executable files and directories. You may need to add this file to .gitignore", - ); + ref: `refs/heads/${branch}`, + path: "new-file.txt", + }) + ).repository?.ref?.target; + + expect(ref).toBeTruthy(); + if (ref && "file" in ref) { + expect(ref.file?.oid).toBeTruthy(); + } }); it("should correctly be able to base changes off specific commit", async () => {