Repository: electron/electron
Pull request: build: rewrite push-patch to use the github API instead of local git commits to ensure commits are signed

Backport of #47401

See that PR for details.


Notes: none

Changed files:
filename: .github/actions/checkout/action.yml
patch: @@ -117,19 +117,19 @@ runs:
         git update-index --refresh || true
         if ! git diff-index --quiet HEAD --; then
           # There are changes to the patches. Make a git commit with the updated patches
-          git add patches
-          GIT_COMMITTER_NAME="PatchUp" GIT_COMMITTER_EMAIL="73610968+patchup[bot]@users.noreply.github.com" git commit -m "chore: update patches" --author="PatchUp <73610968+patchup[bot]@users.noreply.github.com>"
-          # Export it
-          mkdir -p ../../patches
-          git format-patch -1 --stdout --keep-subject --no-stat --full-index > ../../patches/update-patches.patch
-          if node ./script/push-patch.js; then
+          if node ./script/patch-up.js; then
             echo
             echo "======================================================================"
             echo "Changes to the patches when applying, we have auto-pushed the diff to the current branch"
             echo "A new CI job will kick off shortly"
             echo "======================================================================"
             exit 1
           else
+            git add patches
+            GIT_COMMITTER_NAME="PatchUp" GIT_COMMITTER_EMAIL="73610968+patchup[bot]@users.noreply.github.com" git commit -m "chore: update patches" --author="PatchUp <73610968+patchup[bot]@users.noreply.github.com>"
+            # Export it
+            mkdir -p ../../patches
+            git format-patch -1 --stdout --keep-subject --no-stat --full-index > ../../patches/update-patches.patch
             echo
             echo "======================================================================"
             echo "There were changes to the patches when applying."

filename: script/patch-up.js
patch: @@ -0,0 +1,128 @@
+const { appCredentialsFromString, getAuthOptionsForRepo } = require('@electron/github-app-auth');
+
+const { Octokit } = require('@octokit/rest');
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+const { PATCH_UP_APP_CREDS } = process.env;
+
+const REPO_OWNER = 'electron';
+const REPO_NAME = 'electron';
+
+async function getAllPatchFiles (dir) {
+  const files = [];
+
+  async function walkDir (currentDir) {
+    const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
+
+    for (const entry of entries) {
+      const fullPath = path.join(currentDir, entry.name);
+
+      if (entry.isDirectory()) {
+        await walkDir(fullPath);
+      } else if (entry.isFile() && (entry.name.endsWith('.patch') || entry.name === 'README.md' || entry.name === 'config.json')) {
+        const relativePath = path.relative(process.cwd(), fullPath);
+        const content = await fs.promises.readFile(fullPath, 'utf8');
+        files.push({
+          path: relativePath,
+          content
+        });
+      }
+    }
+  }
+
+  await walkDir(dir);
+  return files;
+}
+
+function getCurrentCommitSha () {
+  return process.env.GITHUB_SHA;
+}
+
+function getCurrentBranch () {
+  return process.env.GITHUB_HEAD_REF;
+}
+
+async function main () {
+  if (!PATCH_UP_APP_CREDS) {
+    throw new Error('PATCH_UP_APP_CREDS environment variable not set');
+  }
+
+  const currentBranch = getCurrentBranch();
+
+  if (!currentBranch) {
+    throw new Error('GITHUB_HEAD_REF environment variable not set. Patch Up only works in PR workflows currently.');
+  }
+
+  const octokit = new Octokit({
+    ...await getAuthOptionsForRepo({
+      name: REPO_OWNER,
+      owner: REPO_NAME
+    }, appCredentialsFromString(PATCH_UP_APP_CREDS))
+  });
+
+  const patchesDir = path.join(process.cwd(), 'patches');
+
+  // Get current git state
+  const currentCommitSha = getCurrentCommitSha();
+
+  // Get the tree SHA from the current commit
+  const currentCommit = await octokit.git.getCommit({
+    owner: REPO_OWNER,
+    repo: REPO_NAME,
+    commit_sha: currentCommitSha
+  });
+  const baseTreeSha = currentCommit.data.tree.sha;
+
+  // Find all patch files
+  const patchFiles = await getAllPatchFiles(patchesDir);
+
+  if (patchFiles.length === 0) {
+    throw new Error('No patch files found');
+  }
+
+  console.log(`Found ${patchFiles.length} patch files`);
+
+  // Create a new tree with the patch files
+  const tree = patchFiles.map(file => ({
+    path: file.path,
+    mode: '100644',
+    type: 'blob',
+    content: file.content
+  }));
+
+  const treeResponse = await octokit.git.createTree({
+    owner: REPO_OWNER,
+    repo: REPO_NAME,
+    base_tree: baseTreeSha,
+    tree
+  });
+
+  // Create a new commit
+  const commitMessage = 'chore: update patches';
+  const commitResponse = await octokit.git.createCommit({
+    owner: REPO_OWNER,
+    repo: REPO_NAME,
+    message: commitMessage,
+    tree: treeResponse.data.sha,
+    parents: [currentCommitSha]
+  });
+
+  // Update the branch reference
+  await octokit.git.updateRef({
+    owner: REPO_OWNER,
+    repo: REPO_NAME,
+    ref: `heads/${currentBranch}`,
+    sha: commitResponse.data.sha
+  });
+
+  console.log(`Successfully pushed commit ${commitResponse.data.sha} to ${currentBranch}`);
+}
+
+if (require.main === module) {
+  main().catch((err) => {
+    console.error(err);
+    process.exit(1);
+  });
+}

filename: script/push-patch.js
patch: @@ -1,38 +0,0 @@
-const { appCredentialsFromString, getTokenForRepo } = require('@electron/github-app-auth');
-
-const cp = require('node:child_process');
-
-const { PATCH_UP_APP_CREDS } = process.env;
-
-async function main () {
-  if (!PATCH_UP_APP_CREDS) {
-    throw new Error('PATCH_UP_APP_CREDS environment variable not set');
-  }
-
-  const token = await getTokenForRepo(
-    {
-      name: 'electron',
-      owner: 'electron'
-    },
-    appCredentialsFromString(PATCH_UP_APP_CREDS)
-  );
-
-  const remoteURL = `https://x-access-token:${token}@github.com/electron/electron.git`;
-
-  // NEVER LOG THE OUTPUT OF THIS COMMAND
-  // GIT LEAKS THE ACCESS CREDENTIALS IN CONSOLE LOGS
-  const { status } = cp.spawnSync('git', ['push', '--set-upstream', remoteURL], {
-    stdio: 'ignore'
-  });
-
-  if (status !== 0) {
-    throw new Error('Failed to push to target branch');
-  }
-}
-
-if (require.main === module) {
-  main().catch((err) => {
-    console.error(err);
-    process.exit(1);
-  });
-}