From f56a8c9168b31d54352518e25c452beb9c78f5e0 Mon Sep 17 00:00:00 2001 From: Griffin Baxter <74513598+GriffinBaxterSeequent@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:54:33 +1300 Subject: [PATCH] New GitHub action for updating changelog with PR --- .github/scripts/create-verified-commit.js | 51 +++++------------------ .github/scripts/create-verified-pr.js | 40 ++++++++++++++++++ .github/scripts/git-utils.js | 47 +++++++++++++++++++++ .github/workflows/update-changelog.yml | 50 ++++++++++++++++++++++ 4 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 .github/scripts/create-verified-pr.js create mode 100644 .github/scripts/git-utils.js create mode 100644 .github/workflows/update-changelog.yml diff --git a/.github/scripts/create-verified-commit.js b/.github/scripts/create-verified-commit.js index 45250778..c93b9893 100644 --- a/.github/scripts/create-verified-commit.js +++ b/.github/scripts/create-verified-commit.js @@ -1,65 +1,34 @@ -const fs = require('fs'); -const { execSync } = require('child_process'); +const { createVerifiedCommit } = require('./git-utils'); module.exports = async ({ github, context }) => { if (!context.commitMessage) { throw new Error('commitMessage is required in context'); } - const headSha = execSync('git rev-parse HEAD').toString().trim(); + const { owner, repo } = context.repo; - const changedFiles = execSync('git diff --name-only HEAD').toString().trim().split('\n').filter(f => f); + const commitSha = await createVerifiedCommit({ github, owner, repo, commitMessage: context.commitMessage }); - if (changedFiles.length === 0) { - console.log('No changes to commit'); + if (!commitSha) { return; } - const treeData = []; - for (const file of changedFiles) { - const content = fs.readFileSync(file, 'utf8'); - treeData.push({ - path: file, - mode: '100644', - type: 'blob', - content: content - }); - } - - const tree = await github.rest.git.createTree({ - owner: context.repo.owner, - repo: context.repo.repo, - tree: treeData, - base_tree: headSha - }); - - const commit = await github.rest.git.createCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - message: context.commitMessage, - tree: tree.data.sha, - parents: [headSha] - }); - let targetBranch; const ref = context.ref || ''; if (ref.startsWith('refs/heads/')) { targetBranch = context.ref.split('/').pop(); } else { - const repo = await github.rest.repos.get({ - owner: context.repo.owner, - repo: context.repo.repo - }); - targetBranch = repo.data.default_branch; + const repoData = await github.rest.repos.get({ owner, repo }); + targetBranch = repoData.data.default_branch; } await github.rest.git.updateRef({ - owner: context.repo.owner, - repo: context.repo.repo, + owner, + repo, ref: `heads/${targetBranch}`, - sha: commit.data.sha + sha: commitSha }); - console.log(`Created verified commit: ${commit.data.sha} on branch ${targetBranch}`); + console.log(`Created verified commit: ${commitSha} on branch ${targetBranch}`); }; diff --git a/.github/scripts/create-verified-pr.js b/.github/scripts/create-verified-pr.js new file mode 100644 index 00000000..324c0721 --- /dev/null +++ b/.github/scripts/create-verified-pr.js @@ -0,0 +1,40 @@ +const { createVerifiedCommit } = require('./git-utils'); + +module.exports = async ({ github, context }) => { + if (!context.commitMessage) { + throw new Error('commitMessage is required in context'); + } + if (!context.branchName) { + throw new Error('branchName is required in context'); + } + + const { owner, repo } = context.repo; + + const commitSha = await createVerifiedCommit({ github, owner, repo, commitMessage: context.commitMessage }); + + if (!commitSha) { + return; + } + + const repoData = await github.rest.repos.get({ owner, repo }); + const defaultBranch = repoData.data.default_branch; + + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${context.branchName}`, + sha: commitSha + }); + + const pr = await github.rest.pulls.create({ + owner, + repo, + title: context.prTitle || context.commitMessage, + head: context.branchName, + base: defaultBranch, + body: context.prBody || '' + }); + + console.log(`Created PR #${pr.data.number}: ${pr.data.html_url}`); +}; + diff --git a/.github/scripts/git-utils.js b/.github/scripts/git-utils.js new file mode 100644 index 00000000..c3087e75 --- /dev/null +++ b/.github/scripts/git-utils.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); + +/** + * Creates a verified commit from the current working tree changes using the GitHub API. + * Returns the commit SHA, or null if there are no changes. + */ +async function createVerifiedCommit({ github, owner, repo, commitMessage }) { + const headSha = execSync('git rev-parse HEAD').toString().trim(); + + const changedFiles = execSync('git diff --name-only HEAD').toString().trim().split('\n').filter(f => f); + + if (changedFiles.length === 0) { + console.log('No changes to commit'); + return null; + } + + const treeData = []; + for (const file of changedFiles) { + const content = fs.readFileSync(file, 'utf8'); + treeData.push({ + path: file, + mode: '100644', + type: 'blob', + content: content + }); + } + + const tree = await github.rest.git.createTree({ + owner, + repo, + tree: treeData, + base_tree: headSha + }); + + const commit = await github.rest.git.createCommit({ + owner, + repo, + message: commitMessage, + tree: tree.data.sha, + parents: [headSha] + }); + + return commit.data.sha; +} + +module.exports = { createVerifiedCommit }; diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 00000000..1e6873e5 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,50 @@ +name: "Update Changelog" + +on: + release: + types: [released] + +jobs: + update-changelog: + name: Update CHANGELOG.md + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.tag_name }} + release-notes: ${{ github.event.release.body }} + + - name: Check for changes + id: changes + run: | + if [ -n "$(git status --porcelain CHANGELOG.md)" ]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Create PR with changes + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v8 + with: + script: | + const script = require('${{ github.workspace }}/.github/scripts/create-verified-pr.js'); + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const tagName = '${{ github.event.release.tag_name }}'; + await script({ + github, + context: { + ...context, + repo: { owner, repo }, + commitMessage: `Update CHANGELOG.md for ${tagName}`, + branchName: `update-changelog-${tagName}`, + prTitle: `Update CHANGELOG.md for ${tagName}`, + prBody: `Automated update of CHANGELOG.md for release ${tagName}.` + } + });