Repository: nodejs/node
Pull request: tools: update `create-release-proposal` workflow

We can remove the permission for the repo to `git push`, and instead use a `createCommitOnBranch` API call to create the release commit, which has the upside of being signed.

I've also use this PR as an opportunity to tidy things up (e.g. we don't need a JENKINS_TOKEN, or to fetch the whole repo)
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: .github/workflows/create-release-proposal.yml
patch: @@ -1,7 +1,6 @@
 # This action requires the following secrets to be set on the repository:
 #   GH_USER_NAME: GitHub user whose Jenkins and GitHub token are defined below
 #   GH_USER_TOKEN: GitHub user token, to be used by ncu and to push changes
-#   JENKINS_TOKEN: Jenkins token, to be used to check CI status
 
 name: Create Release Proposal
 
@@ -24,6 +23,7 @@ env:
 
 permissions:
   contents: write
+  pull-requests: write
 
 jobs:
   releasePrepare:
@@ -37,9 +37,7 @@ jobs:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
         with:
           ref: ${{ env.STAGING_BRANCH }}
-          # Needs the whole git history for ncu to work
-          # See https://github.com/nodejs/node-core-utils/pull/486
-          fetch-depth: 0
+          persist-credentials: false
 
       # Install dependencies
       - name: Install Node.js
@@ -56,29 +54,32 @@ jobs:
           ncu-config set upstream origin
           ncu-config set username "$USERNAME"
           ncu-config set token "$GH_TOKEN"
-          ncu-config set jenkins_token "$JENKINS_TOKEN"
           ncu-config set repo "$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)"
           ncu-config set owner "${GITHUB_REPOSITORY_OWNER}"
         env:
           USERNAME: ${{ secrets.JENKINS_USER }}
-          GH_TOKEN: ${{ secrets.GH_USER_TOKEN }}
-          JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }}
+          GH_TOKEN: ${{ github.token }}
 
       - name: Set up ghauth config (Ubuntu)
         run: |
-          mkdir -p ~/.config/changelog-maker/
-          echo '{
-            "user": "'$(ncu-config get username)'",
-            "token": "'$(ncu-config get token)'"
-          }' > ~/.config/changelog-maker/config.json
+          mkdir -p "${XDG_CONFIG_HOME:-~/.config}/changelog-maker"
+          echo '{}' | jq '{user: env.USERNAME, token: env.TOKEN}' > "${XDG_CONFIG_HOME:-~/.config}/changelog-maker/config.json"
+        env:
+          USERNAME: ${{ secrets.JENKINS_USER }}
+          TOKEN: ${{ github.token }}
 
       - name: Setup git author
         run: |
           git config --local user.email "github-bot@iojs.org"
           git config --local user.name "Node.js GitHub Bot"
 
       - name: Start git node release prepare
+        # The curl command is to make sure we run the version of the script corresponding to the current workflow.
         run: |
+          git update-index --assume-unchanged tools/actions/create-release.sh
+          curl -fsSLo tools/actions/create-release.sh https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/tools/actions/create-release.sh
           ./tools/actions/create-release.sh "${RELEASE_DATE}" "${RELEASE_LINE}"
         env:
-          GH_TOKEN: ${{ secrets.GH_USER_TOKEN }}
+          GH_TOKEN: ${{ github.token }}
+          # We want the bot to push the push the release commit so CI runs on it.
+          BOT_TOKEN: ${{ secrets.GH_USER_TOKEN }}

filename: tools/actions/create-release.sh
patch: @@ -2,6 +2,9 @@
 
 set -xe
 
+GITHUB_REPOSITORY=${GITHUB_REPOSITORY:-nodejs/node}
+BOT_TOKEN=${BOT_TOKEN:-}
+
 RELEASE_DATE=$1
 RELEASE_LINE=$2
 
@@ -10,24 +13,114 @@ if [ -z "$RELEASE_DATE" ] || [ -z "$RELEASE_LINE" ]; then
   exit 1
 fi
 
+if [ -z "$GITHUB_REPOSITORY" ] || [ -z "$BOT_TOKEN" ]; then
+  echo "Invalid value in env for GITHUB_REPOSITORY and BOT_TOKEN"
+  exit 1
+fi
+
+if ! command -v node || ! command -v gh || ! command -v git || ! command -v awk; then
+  echo "Missing required dependencies"
+  exit 1
+fi
+
 git node release --prepare --skipBranchDiff --yes --releaseDate "$RELEASE_DATE"
-# We use it to not specify the branch name as it changes based on
-# the commit list (semver-minor/semver-patch)
-git config push.default current
-git push
+
+HEAD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+HEAD_SHA="$(git rev-parse HEAD^)"
 
 TITLE=$(awk "/^## ${RELEASE_DATE}/ { print substr(\$0, 4) }" "doc/changelogs/CHANGELOG_V${RELEASE_LINE}.md")
 
 # Use a temporary file for the PR body
 TEMP_BODY="$(awk "/## ${RELEASE_DATE}/,/^<a id=/{ if (!/^<a id=/) print }" "doc/changelogs/CHANGELOG_V${RELEASE_LINE}.md")"
 
-PR_URL="$(gh pr create --title "$TITLE" --body "$TEMP_BODY" --base "v$RELEASE_LINE.x")"
+# Create the proposal branch
+gh api \
+  --method POST \
+  -H "Accept: application/vnd.github+json" \
+  -H "X-GitHub-Api-Version: 2022-11-28" \
+  "/repos/${GITHUB_REPOSITORY}/git/refs" \
+   -f "ref=refs/heads/$HEAD_BRANCH" -f "sha=$HEAD_SHA"
+
+# Create the proposal PR
+PR_URL="$(gh api \
+  --method POST \
+  --jq .html_url \
+  -H "Accept: application/vnd.github+json" \
+  -H "X-GitHub-Api-Version: 2022-11-28" \
+  "/repos/${GITHUB_REPOSITORY}/pulls" \
+   -f "title=$TITLE" -f "body=$TEMP_BODY" -f "head=$HEAD_BRANCH" -f "base=v$RELEASE_LINE.x")"
 
-# Amend commit message so it contains the correct PR-URL trailer.
-AMENDED_COMMIT_MSG="$(git log -1 --pretty=%B | sed "s|PR-URL: TODO|PR-URL: $PR_URL|")"
+# Push the release commit to the proposal branch using `BOT_TOKEN` from the env
+node --input-type=module - \
+    "$GITHUB_REPOSITORY" \
+    "$HEAD_BRANCH" \
+    "$HEAD_SHA" \
+    "$(git log -1 HEAD --format=%s || true)" \
+    "$(git log -1 HEAD --format=%b | awk -v PR_URL="$PR_URL" '{sub(/^PR-URL: TODO$/, "PR-URL: " PR_URL)} 1' || true)" \
+    "$(git show HEAD --diff-filter=d --name-only --format= || true)" \
+    "$(git show HEAD --diff-filter=D --name-only --format= || true)" \
+<<'EOF'
+const [,,
+  repo,
+  branch,
+  parentCommitSha,
+  commit_title,
+  commit_body,
+  modifiedOrAddedFiles,
+  deletedFiles,
+] = process.argv;
 
-# Replace "TODO" with the PR URL in the last commit
-git commit --amend --no-edit -m "$AMENDED_COMMIT_MSG" || true
+import { readFileSync } from 'node:fs';
+import util from 'node:util';
 
-# Force-push the amended commit
-git push --force
+const query = `
+mutation ($repo: String! $branch: String!, $parentCommitSha: GitObjectID!, $changes: FileChanges!, $commit_title: String!, $commit_body: String) {
+  createCommitOnBranch(input: {
+    branch: {
+      repositoryNameWithOwner: $repo,
+      branchName: $branch
+    },
+    message: {
+      headline: $commit_title,
+      body: $commit_body
+    },
+    expectedHeadOid: $parentCommitSha,
+    fileChanges: $changes
+  }) {
+    commit {
+      url
+    }
+  }
+}
+`;
+const response = await fetch('https://api.github.com/graphql', {
+  method: 'POST',
+  headers: {
+    'Authorization': `bearer ${process.env.BOT_TOKEN}`,
+  },
+  body: JSON.stringify({
+    query,
+    variables: {
+      repo,
+      branch,
+      parentCommitSha,
+      commit_title,
+      commit_body,
+      changes: {
+        additions: modifiedOrAddedFiles.split('\n').filter(Boolean)
+          .map(path => ({ path, contents: readFileSync(path).toString('base64') })),
+        deletions: deletedFiles.split('\n').filter(Boolean),
+      }
+    },
+  })
+});
+if (!response.ok) {
+  console.log({statusCode: response.status, statusText: response.statusText});
+  process.exitCode ||= 1;
+}
+const data = await response.json();
+if (data.errors?.length) {
+  throw new Error('Endpoint returned an error', { cause: data });
+}
+console.log(util.inspect(data, { depth: Infinity }));
+EOF