Skip to content

Commit f109ff0

Browse files
authored
Handle submodule entries in push_signed_commits by falling back to git push (#26298)
1 parent a7205dc commit f109ff0

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

actions/setup/js/push_signed_commits.cjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
152152
// The GitHub GraphQL createCommitOnBranch mutation only supports regular file mode 100644:
153153
// - Symlinks (120000) would be silently converted to regular files containing the link target path
154154
// - Executable bits (100755) are silently dropped
155+
// - Submodules/gitlinks (160000) are not supported; the mutation does not accept commit-object entries
155156
/** @type {Map<string, Array<{path: string, contents: string}>>} */
156157
const additionsMap = new Map();
157158
/** @type {Map<string, Array<{path: string}>>} */
@@ -180,13 +181,19 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
180181
core.warning(`pushSignedCommits: unexpected diff-tree output format, skipping line: ${line}`);
181182
continue;
182183
}
183-
const dstMode = modeFields[1]; // destination file mode (e.g. 100644, 100755, 120000)
184+
const srcMode = modeFields[0]; // source file mode (e.g. 100644, 100755, 120000, 160000)
185+
const dstMode = modeFields[1]; // destination file mode (e.g. 100644, 100755, 120000, 160000)
184186
const status = modeFields[4]; // A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied
185187

186188
const paths = line.slice(tabIdx + 1).split("\t");
187189
const filePath = unquoteCPath(paths[0]);
188190

189191
if (status === "D") {
192+
// mode 160000 = gitlink (submodule); GitHub GraphQL createCommitOnBranch does not support submodules
193+
if (srcMode === "160000") {
194+
core.warning(`pushSignedCommits: submodule change detected in ${filePath}, falling back to git push`);
195+
throw new Error("submodule change detected");
196+
}
190197
deletions.push({ path: filePath });
191198
} else if (status && status.startsWith("R")) {
192199
// Rename: source path is deleted, destination path is added
@@ -196,6 +203,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
196203
continue;
197204
}
198205
deletions.push({ path: filePath });
206+
if (srcMode === "160000" || dstMode === "160000") {
207+
core.warning(`pushSignedCommits: submodule change detected in ${filePath} -> ${renamedPath}, falling back to git push`);
208+
throw new Error("submodule change detected");
209+
}
199210
if (dstMode === "120000") {
200211
core.warning(`pushSignedCommits: symlink ${renamedPath} cannot be pushed as a signed commit, falling back to git push`);
201212
throw new Error("symlink file mode requires git push fallback");
@@ -211,6 +222,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
211222
core.warning(`pushSignedCommits: copy entry missing destination path, skipping: ${line}`);
212223
continue;
213224
}
225+
if (dstMode === "160000") {
226+
core.warning(`pushSignedCommits: submodule change detected in ${copiedPath}, falling back to git push`);
227+
throw new Error("submodule change detected");
228+
}
214229
if (dstMode === "120000") {
215230
core.warning(`pushSignedCommits: symlink ${copiedPath} cannot be pushed as a signed commit, falling back to git push`);
216231
throw new Error("symlink file mode requires git push fallback");
@@ -221,6 +236,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
221236
additions.push({ path: copiedPath, contents: await readBlobAsBase64(sha, copiedPath, cwd) });
222237
} else {
223238
// Added or Modified
239+
if (dstMode === "160000") {
240+
core.warning(`pushSignedCommits: submodule change detected in ${filePath}, falling back to git push`);
241+
throw new Error("submodule change detected");
242+
}
224243
if (dstMode === "120000") {
225244
core.warning(`pushSignedCommits: symlink ${filePath} cannot be pushed as a signed commit, falling back to git push`);
226245
throw new Error("symlink file mode requires git push fallback");

actions/setup/js/push_signed_commits.test.cjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,43 @@ describe("push_signed_commits integration tests", () => {
766766
expect(Buffer.from(callArg.fileChanges.additions[0].contents, "base64").toString()).toContain("echo hello");
767767
});
768768

769+
it("should fall back to git push and warn when commit contains a submodule entry", async () => {
770+
execGit(["checkout", "-b", "submodule-branch"], { cwd: workDir });
771+
772+
// Create a gitlink (mode 160000) entry directly via update-index so we don't
773+
// need a real submodule URL. git diff-tree --raw will report this as mode 160000.
774+
// The cacheinfo format is: <mode>,<objectId>,<path>
775+
const headSha = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim();
776+
execGit(["update-index", "--add", "--cacheinfo", `160000,${headSha},mysubmodule`], { cwd: workDir });
777+
execGit(["commit", "-m", "Add submodule"], { cwd: workDir });
778+
execGit(["push", "-u", "origin", "submodule-branch"], { cwd: workDir });
779+
780+
global.exec = makeRealExec(workDir);
781+
const githubClient = makeMockGithubClient();
782+
783+
await pushSignedCommits({
784+
githubClient,
785+
owner: "test-owner",
786+
repo: "test-repo",
787+
branch: "submodule-branch",
788+
// Only replay the submodule commit
789+
baseRef: "submodule-branch^",
790+
cwd: workDir,
791+
});
792+
793+
// GraphQL should NOT have been called – submodule triggers fallback before mutation
794+
expect(githubClient.graphql).not.toHaveBeenCalled();
795+
// Warning about submodule must be emitted
796+
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("submodule change detected in mysubmodule"));
797+
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("falling back to git push"));
798+
799+
// The commit should be present on the remote via git push fallback
800+
const lsRemote = execGit(["ls-remote", bareDir, "refs/heads/submodule-branch"], { cwd: workDir });
801+
const remoteOid = lsRemote.stdout.trim().split(/\s+/)[0];
802+
const localOid = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim();
803+
expect(remoteOid).toBe(localOid);
804+
});
805+
769806
it("should not warn for regular files (mode 100644)", async () => {
770807
execGit(["checkout", "-b", "regular-file-branch"], { cwd: workDir });
771808
fs.writeFileSync(path.join(workDir, "regular.txt"), "Regular file content\n");

0 commit comments

Comments
 (0)