Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion actions/setup/js/push_signed_commits.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
// The GitHub GraphQL createCommitOnBranch mutation only supports regular file mode 100644:
// - Symlinks (120000) would be silently converted to regular files containing the link target path
// - Executable bits (100755) are silently dropped
// - Submodules/gitlinks (160000) are not supported; the mutation does not accept commit-object entries
/** @type {Map<string, Array<{path: string, contents: string}>>} */
const additionsMap = new Map();
/** @type {Map<string, Array<{path: string}>>} */
Expand Down Expand Up @@ -157,13 +158,19 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
core.warning(`pushSignedCommits: unexpected diff-tree output format, skipping line: ${line}`);
continue;
}
const dstMode = modeFields[1]; // destination file mode (e.g. 100644, 100755, 120000)
const srcMode = modeFields[0]; // source file mode (e.g. 100644, 100755, 120000, 160000)
const dstMode = modeFields[1]; // destination file mode (e.g. 100644, 100755, 120000, 160000)
const status = modeFields[4]; // A=Added, M=Modified, D=Deleted, R=Renamed, C=Copied

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

if (status === "D") {
// mode 160000 = gitlink (submodule); GitHub GraphQL createCommitOnBranch does not support submodules
if (srcMode === "160000") {
core.warning(`pushSignedCommits: submodule change detected in ${filePath}, falling back to git push`);
throw new Error("submodule change detected");
}
Comment on lines 168 to +173
deletions.push({ path: filePath });
} else if (status && status.startsWith("R")) {
// Rename: source path is deleted, destination path is added
Expand All @@ -173,6 +180,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
continue;
}
deletions.push({ path: filePath });
if (srcMode === "160000" || dstMode === "160000") {
core.warning(`pushSignedCommits: submodule change detected in ${filePath} -> ${renamedPath}, falling back to git push`);
throw new Error("submodule change detected");
}
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${renamedPath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
Expand All @@ -189,6 +200,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
core.warning(`pushSignedCommits: copy entry missing destination path, skipping: ${line}`);
continue;
}
if (dstMode === "160000") {
core.warning(`pushSignedCommits: submodule change detected in ${copiedPath}, falling back to git push`);
throw new Error("submodule change detected");
}
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${copiedPath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
Expand All @@ -200,6 +215,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
additions.push({ path: copiedPath, contents: content.toString("base64") });
} else {
// Added or Modified
if (dstMode === "160000") {
core.warning(`pushSignedCommits: submodule change detected in ${filePath}, falling back to git push`);
throw new Error("submodule change detected");
}
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${filePath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
Expand Down
37 changes: 37 additions & 0 deletions actions/setup/js/push_signed_commits.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,43 @@ describe("push_signed_commits integration tests", () => {
expect(Buffer.from(callArg.fileChanges.additions[0].contents, "base64").toString()).toContain("echo hello");
});

it("should fall back to git push and warn when commit contains a submodule entry", async () => {
execGit(["checkout", "-b", "submodule-branch"], { cwd: workDir });

// Create a gitlink (mode 160000) entry directly via update-index so we don't
// need a real submodule URL. git diff-tree --raw will report this as mode 160000.
// The cacheinfo format is: <mode>,<objectId>,<path>
const headSha = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim();
execGit(["update-index", "--add", "--cacheinfo", `160000,${headSha},mysubmodule`], { cwd: workDir });
execGit(["commit", "-m", "Add submodule"], { cwd: workDir });
execGit(["push", "-u", "origin", "submodule-branch"], { cwd: workDir });

global.exec = makeRealExec(workDir);
const githubClient = makeMockGithubClient();

await pushSignedCommits({
githubClient,
owner: "test-owner",
repo: "test-repo",
branch: "submodule-branch",
// Only replay the submodule commit
baseRef: "submodule-branch^",
cwd: workDir,
});

// GraphQL should NOT have been called – submodule triggers fallback before mutation
expect(githubClient.graphql).not.toHaveBeenCalled();
// Warning about submodule must be emitted
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("submodule change detected in mysubmodule"));
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("falling back to git push"));

// The commit should be present on the remote via git push fallback
const lsRemote = execGit(["ls-remote", bareDir, "refs/heads/submodule-branch"], { cwd: workDir });
const remoteOid = lsRemote.stdout.trim().split(/\s+/)[0];
const localOid = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim();
expect(remoteOid).toBe(localOid);
});

it("should not warn for regular files (mode 100644)", async () => {
execGit(["checkout", "-b", "regular-file-branch"], { cwd: workDir });
fs.writeFileSync(path.join(workDir, "regular.txt"), "Regular file content\n");
Expand Down