Skip to content

Apply assorted non-functional improvements #606

Apply assorted non-functional improvements

Apply assorted non-functional improvements #606

name: Suggest PR commit message
on:
pull_request:
types:
- edited
- opened
- reopened
- synchronize
workflow_dispatch:
inputs:
pr_number:
description: Pull request number of interest
required: true
type: number
permissions:
contents: read
concurrency:
group: suggest-commit-message-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
env:
ALLOWED_ENDPOINTS: >
api.github.com:443
api.openai.com:443
github.com:443
registry.npmjs.org:443
jobs:
suggest:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-24.04
environment: codex
steps:
- name: Install Harden-Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
# We can't disable `sudo`, as `openai/codex-action` unconditionally
# invokes `sudo`, even with `safety-strategy: unsafe` and
# `sandbox: danger-full-access`.
# XXX: Consider splitting this workflow into three jobs, with
# `openai/codex-action` being the sole step of the second job.
disable-sudo-and-containers: false
egress-policy: block
allowed-endpoints: ${{ env.ALLOWED_ENDPOINTS }}
- name: Resolve pull request metadata
id: pr-details
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = Number(context.payload.pull_request?.number ?? context.payload.inputs?.pr_number);
if (!Number.isFinite(prNumber) || prNumber <= 0) {
throw new Error('Unable to determine pull request number');
}
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
core.setOutput('number', String(pr.number));
core.setOutput('title', pr.title ?? '');
core.setOutput('body', pr.body ?? '');
core.setOutput('author', pr.user?.login ?? '');
core.setOutput('baseRef', pr.base.ref ?? '');
core.setOutput('baseSha', pr.base.sha ?? '');
core.setOutput('headRef', pr.head.ref ?? '');
core.setOutput('headSha', pr.head.sha ?? '');
- name: Check out pull request head
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ steps.pr-details.outputs.headSha }}
fetch-depth: 0
- name: Prepare Codex prompt
id: prompt
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ steps.pr-details.outputs.number }}
PR_TITLE: ${{ steps.pr-details.outputs.title }}
PR_BODY: ${{ steps.pr-details.outputs.body }}
PR_AUTHOR: ${{ steps.pr-details.outputs.author }}
BASE_REF: ${{ steps.pr-details.outputs.baseRef }}
BASE_SHA: ${{ steps.pr-details.outputs.baseSha }}
HEAD_REF: ${{ steps.pr-details.outputs.headRef }}
HEAD_SHA: ${{ steps.pr-details.outputs.headSha }}
with:
script: |
const fs = require('fs');
const env = process.env;
const repository = env.REPOSITORY;
const prNumber = env.PR_NUMBER;
const title = env.PR_TITLE;
const body = env.PR_BODY;
const author = env.PR_AUTHOR;
const baseRef = env.BASE_REF;
const baseSha = env.BASE_SHA;
const headRef = env.HEAD_REF;
const headSha = env.HEAD_SHA;
const cleanedBody = (body || '').trim() || '<no pull request description>';
// Determine whether this is an upgrade PR.
const upgradeMatch = title?.match(/^Upgrade (.+?) \S+ -> \S+/);
const upgradeLibrary = upgradeMatch ? upgradeMatch[1] : null;
// Extract domain names from list of endpoints to which Harden-Runner will allow access.
const allowedDomains = process.env.ALLOWED_ENDPOINTS.split(/\s+/).map(line => line.split(':')[0]).filter(Boolean);
const instructions = `
You are an experienced maintainer helping to craft the squash commit message for a GitHub pull request.
Pull request metadata:
- Repository: ${repository}
- Number: ${prNumber}
- Title: ${title}
- Author: ${author}
- Base branch: ${baseRef} (${baseSha})
- Head branch: ${headRef} (${headSha})
Pull request description:
\`\`\`
${cleanedBody}
\`\`\`
Requirements:
1. Write the summary line in the imperative mood. Try not to exceed 80 characters.
2. End the summary line with the PR number in parentheses, i.e., " (#${prNumber})".
3. Wrap each body paragraph at 72 characters. Focus on the "what" and "why" rather than implementation details.
4. Keep the overall message concise.
5. Match the established format used in similar past commits.
6. Wrap code references in backticks.
7. For dependency upgrades in particular, *very precisely* follow the pattern of past commit messages: reuse the summary wording (only adjust version numbers) and list updated changelog, release note, and diff URLs in the body.
8. Don't hallucinate URLs, version numbers, or other factual information.
9. Never split URLs across multiple lines, even if they exceed 72 characters.
10. If the pull request description already contains a suitable commit message, prefer using that as-is.
To help you craft an appropriate commit message, execute the following commands to gather context:
1. Get the changed files:
\`\`\`
git diff --name-status ${baseSha}...${headSha}
\`\`\`
2. Get a diff excerpt (first 500 lines):
\`\`\`
git diff ${baseSha}...${headSha} | head -500
\`\`\`
${upgradeLibrary ? `3. Since this appears to be an upgrade PR for ${upgradeLibrary}, collect relevant past upgrade commit messages, and consider the general style of other recent upgrade commit messages:
\`\`\`
git log -P -i --grep '^Upgrade \\Q${upgradeLibrary}\\E' --pretty='format:%h %B%n---' -n 20 ${baseSha}
git log -P -i --grep '^Upgrade (?!\\Q${upgradeLibrary}\\E)' --pretty='format:%h %B%n---' -n 150 ${baseSha}
\`\`\`
4. If this is a GitHub-hosted library, collect milestones and release candidates that may not be included in the changelog:
\`\`\`
curl -s "https://api.github.com/repos/{owner}/{repo}/releases?per_page=100" | jq -r '.[].tag_name' | sort -h | tail -n 50
\`\`\`` : `3. Get examples of recent non-upgrade commits:
\`\`\`
git log --grep '^Upgrade' --invert-grep --pretty='format:%h %B%n---' -n 50 ${baseSha}
\`\`\``}
Some further guidelines to help you craft good upgrade commit messages:
- Unless highly salient, don't summarize code changes made as part of the upgrade.
- Don't bother linking to anchors within changelogs or release notes; just link to the main page.
- For GitHub-hosted projects, always link to all relevant GitHub release pages, including those for milestones, release candidates and other intermediate versions. This is especially important for major and minor version upgrades of the following libraries:
- Jackson
- JUnit
- Micrometer
- Project Reactor
- Spring Framework
- Spring Boot
- Spring Security
- For GitHub-hosted projects, always link to the full diff between versions.
- Enumerate links in the following order:
1. First, link to custom release note documents.
2. Then list all GitHub release links in ascending order.
3. Finally, provide the full diff link.
- If the upgrade involves multiple dependencies, group the links by dependency.
- When the Maven \u0060version.error-prone-orig\u0060 property is changed, this upgrades both Error Prone and Picnic's Error Prone fork. In this case:
- Make sure that the commit message includes a diff URL for the latter.
- Don't explicitly mention that \u0060version.error-prone-orig\u0060 got changed; just focus on the fact that Error Prone is being upgraded.
- For major and minor version upgrades, check past dependency upgrade commit messages to infer documentation, blog or wiki URLs to which to link. Do this for at least the following libraries:
- Jackson: https://github.com/FasterXML/jackson/wiki/Jackson-Release-{version}
- Spring Framework: https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-{version}-Release-Notes
- Spring Boot: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-{version}-Release-Notes
- Spring Security: https://docs.spring.io/spring-security/reference/{version}/whats-new.html
- If you really can't find relevant URLs to reference, and there's nothing else to say, it's acceptable to have a commit message that only consists of the summary line.
Note that your network access is limited to the following domains; don't attempt \`curl\` or \`wget\` commands to other hosts:
${allowedDomains.map(domain => `- ${domain}`).join('\n')}
Return a JSON object with the following shape:
{
"summary": "<summary line>",
"body": "<commit body with paragraphs wrapped at 72 characters, or empty string>"
}
Ensure the JSON is valid. Do not include additional commentary outside the JSON structure.
`;
const promptPath = '/tmp/codex-prompt-suggest-commit-message.md';
fs.writeFileSync(promptPath, instructions.trim() + '\n', { encoding: 'utf8' });
- name: Suggest commit message with Codex
id: codex
uses: openai/codex-action@086169432f1d2ab2f4057540b1754d550f6a1189 # v1.4
with:
# XXX: We're using `safety-strategy: unsafe` and
# `sandbox: danger-full-access` so that the agent is able to access
# the network and look up e.g. GitHub release tags. Some amount of
# safety is provided by the Harden-Runner step further up.
safety-strategy: unsafe
sandbox: danger-full-access
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: /tmp/codex-prompt-suggest-commit-message.md
output-schema: |
{
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Summary line in imperative mood, preferably at most 72 characters"
},
"body": {
"type": "string",
"description": "Commit message body explaining what and why, wrapped at 72 characters"
}
},
"required": ["summary", "body"],
"additionalProperties": false
}
- name: Upsert pull request comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBER: ${{ steps.pr-details.outputs.number }}
CODEX_RESULT: ${{ steps.codex.outputs.final-message }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const prNumber = process.env.PR_NUMBER;
const codexResult = JSON.parse(process.env.CODEX_RESULT);
const summary = codexResult.summary.trim();
const body = codexResult.body.trim();
const commitMessage = body ? `${summary}\n\n${body}` : summary;
// Write the commit message to a file, so that the next step can
// attach it as a workflow artifact for debug purposes.
fs.writeFileSync('/tmp/suggested-commit-message.txt', commitMessage, { encoding: 'utf8' });
// The comment to be upserted includes a hidden marker to identify it.
const marker = '<!-- codex-suggested-commit-message -->';
const commentBody = `Suggested commit message:\n${marker}\n\n\`\`\`\n${commitMessage}\n\`\`\`\n`;
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find((comment) => comment.body?.includes(marker));
if (!existing) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody,
});
core.info('Created new commit message suggestion comment.');
return;
}
if (existing.body === commentBody) {
core.info('Existing comment already up to date.');
return;
}
// Determine who, if anybody, last edited the existing comment.
const commentNode = await github.graphql(
`query ($id: ID!) {
node(id: $id) {
... on IssueComment {
editor {
login
}
}
}
}`,
{ id: existing.node_id },
);
// If another user last edited the comment, skip the update. Note that the `[bot]` suffix is stripped
// because it does not seem to be present consistently.
const originalCommenter = existing.user.login.replace(/\[bot\]$/, '');
const lastEditor = commentNode.node.editor?.login?.replace(/\[bot\]$/, '');
if (lastEditor && lastEditor !== originalCommenter) {
core.info(
`Skipping update because comment was last edited by ${lastEditor} rather than ${originalCommenter}.`,
);
return;
}
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: commentBody,
});
core.info(`Updated comment ${existing.id} by ${originalCommenter}.`);
- name: Upload suggested commit message
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: suggested-commit-message
path: /tmp/suggested-commit-message.txt