Skip to content

Commit

Permalink
feat(changelog): update email retrieval for GitHub username (#70)
Browse files Browse the repository at this point in the history
Updated the changelog.js file to use the correct variable for
retrieving the GitHub username from the email address. This ensures
that the application accurately identifies the user.

Additionally, created and leveraged a new release-preview internal
GitHub application for generating pull requests that allow subsequent
workflows to trigger, unlike the previous method using the
github-actions[bot]. Migrated the pull request creation to utilize a
3rd party action that integrates with the new release-preview app.

Also, tweaked the prompt for AI release notes generation for better
user experience.
  • Loading branch information
virgofx authored Oct 24, 2024
1 parent 8a9ebcc commit 53d079e
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 109 deletions.
61 changes: 37 additions & 24 deletions .github/scripts/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,32 @@ Ignore merge commits and minor changes. For each commit, use only the first line
Translate Conventional Commit messages into professional, human-readable language, avoiding technical jargon.
For each commit, use this format:
- **Bold 3-5 word Summary** (with related GitHub emoji): Continuation with 1-3 sentence description. @author (optional #PR)
- **Bold 3-5 word Summary** {optional related GitHub emoji}: Continuation with 1-3 sentence description. @author (optional #PR)
- Sub-bullets for key details (include only if necessary)
Important formatting rules:
- Place PR/issue numbers matching the exact pattern #\d+ (e.g., #123) at the end of the section in parentheses.
- Do not use commit hashes as PR numbers
- If no PR/issue number is found matching #\\d+, omit the parenthetical reference entirely
- If the author is specified, include their GitHub username at the end of the section, just before the PR/issue number with a "@" symbol - e.g. @author.
- If the author is not specified, omit the GitHub username.
- Only include sub-bullets if they are necessary to clarify the change.
- Avoid level 4 headings.
- Use level 3 (###) for sections.
- Omit sections with no content silently - do not add any notes or explanations about omitted sections.
Place PR/issue numbers matching the exact pattern #\d+ (e.g., #123) at the end of the section in parentheses.
Do not use commit hashes as PR numbers.
If no PR/issue number is found matching #\\d+, omit the parenthetical reference entirely.
If the author is specified, include their GitHub username at the end of the section, just before the PR/issue number with a "@" symbol - e.g. @author.
If the author is not specified, omit the GitHub username.
Only include sub-bullets if they are necessary to clarify the change.
Do not include any sections with no content.
Do not include sections where there are no grouped changes.
Do not include sections where content is similar to "No breaking changes in this release".
Avoid level 4 headings; use level 3 (###) for sections.
Attempt to add an emoji into the {optional related GitHub emoji} section of the summary that relates to the bold-3-5 word summary and 1-3 sentence description.
Omit sections with no content silently - do not add any notes or explanations about omitted sections.
`;

// In-memory cache for username lookups
Expand Down Expand Up @@ -153,11 +166,11 @@ async function githubApiRequestWithRetry(path, retries = 2) {
* Attempts to resolve a GitHub username from a commit email address
* using multiple GitHub API endpoints.
*
* @param {string} commitEmail - The email address from the git commit
* @param {string} email - The email address from the git commit
* @returns {Promise<string|null>} - GitHub username if found, null otherwise
*/
async function resolveGitHubUsername(commitEmail) {
console.log('Attempting to resolve username:', commitEmail);
async function resolveGitHubUsername(email) {
console.log('Attempting to resolve username:', email);

// Local resolution - Handle various GitHub email patterns
const emailMatches = email.match(/^(?:(?:[^@]+)?@)?([^@]+)$/);
Expand Down Expand Up @@ -194,38 +207,38 @@ async function resolveGitHubUsername(commitEmail) {

try {
// First attempt: Direct API search for user by email
console.log(`[${commitEmail}] Querying user API`);
console.log(`[${email}] Querying user API`);
const searchResponse = await githubApiRequestWithRetry(
`https://api.github.com/search/users?q=${encodeURIComponent(commitEmail)}+in:email`,
`https://api.github.com/search/users?q=${encodeURIComponent(email)}+in:email`,
);
if (searchResponse?.items && searchResponse.items.length > 0) {
console.log(`[${commitEmail}] Found username`);
console.log(`[${email}] Found username`);
// Get the first matching user
return searchResponse.items[0].login;
}
console.log(`[${commitEmail}] No username found via user API`);
console.log(`[${email}] No username found via user API`);
} catch (error) {
console.error(`[${commitEmail}] Error resolving GitHub username via user API:`, error);
console.error(`[${email}] Error resolving GitHub username via user API:`, error);
}

try {
console.log(`[${commitEmail}] Querying commit API`);
console.log(`[${email}] Querying commit API`);
// Second attempt: Check commit API for associated username
const commitSearchResponse = await githubApiRequestWithRetry(
`https://api.github.com/search/commits?q=author-email:${encodeURIComponent(commitEmail)}&per_page=25`,
`https://api.github.com/search/commits?q=author-email:${encodeURIComponent(email)}&per_page=25`,
);
if (commitSearchResponse?.items?.length > 0) {
// Loop through all items looking for first commit with an author
for (const commit of commitSearchResponse.items) {
if (commit.author) {
console.log(`[${commitEmail}] Found username from commit ${commit.sha}`);
console.log(`[${email}] Found username from commit ${commit.sha}`);
return commit.author.login;
}
}
console.log(`[${commitEmail}] No commits with author found in ${commitSearchResponse.items.length} results`);
console.log(`[${email}] No commits with author found in ${commitSearchResponse.items.length} results`);
}
} catch (error) {
console.error(`[${commitEmail}] Error resolving GitHub username via commit API:`, error);
console.error(`[${email}] Error resolving GitHub username via commit API:`, error);
}

return null;
Expand Down
128 changes: 44 additions & 84 deletions .github/workflows/release-start.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ jobs:
name: release
runs-on: ubuntu-latest
permissions:
contents: write # Required to create a new pull request
pull-requests: write # Required to comment on pull requests
actions: write # Required to trigger workflows
contents: read # Required to read repo contents. Note: We leverage release-preview app for PR + commit generation
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Get all history
fetch-depth: 0 # Get all history which is required for parsing commits

- name: Setup Node.js
id: setup-node
Expand All @@ -41,71 +39,64 @@ jobs:
id: npm-ci
run: npm ci --no-fund

# In order to create signed commits, we need to ensure that we commit without an author name and email.
# However, this can't be done via git as this is required. We need to leverage the GitHub REST/GraphQL
# API endpoints.
# https://github.com/orgs/community/discussions/24664#discussioncomment-5084236
- name: Setup ghup [GitHub API Client]
uses: nexthink-oss/ghup/actions/setup@main
with:
version: v0.11.2

- name: Create new release branch
run: |
# Delete the branch if it exists on remote
if git ls-remote --exit-code --heads origin ${{ env.BRANCH_NAME }}; then
echo "Deleting existing branch ${{ env.BRANCH_NAME }}."
git push origin --delete ${{ env.BRANCH_NAME }}
fi
# Create a new branch and checkout
git checkout -b ${{ env.BRANCH_NAME }}
# Rebase the branch onto main (or whatever the base branch is)
git rebase origin/main
- name: Update package.json version
run: npm version ${{ env.VERSION }} --no-git-tag-version

- name: Build the package
run: npm run package

- name: Commit Changes (via API) using ghup
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHUP_MESSAGE: "chore(release): bump version to ${{ env.VERSION }}"
run: |
ghup content dist/* package.json package-lock.json \
--trailer "Release-Initiated-By=${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>" \
--trailer "Build-Logs=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--trailer "Co-Authored-By=github-actions[bot] <github-actions[bot]@users.noreply.github.com>" \
--trailer "Release=v${{ env.VERSION }}"
- name: Generate Changelog
uses: actions/github-script@v7
id: changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
with:
result-encoding: json
script: |
const { generateChangelog } = await import('${{ github.workspace }}/.github/scripts/changelog.js');
try {
const changelog = await generateChangelog("${{ env.VERSION }}");
console.log('Generated changelog:', changelog);
return changelog;
} catch (error) {
console.error('Error generating changelog:', error);
core.setFailed(error.message);
}
# Pull requests created by the action using the default GITHUB_TOKEN cannot trigger other workflows.
# If you have on: pull_request or on: push workflows acting as checks on pull requests, they will not run.
#
# See below for additional documentation on workarounds:
#
# https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs

- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.RELEASE_PREVIEW_APP_ID }}
private-key: ${{ secrets.RELEASE_PREVIEW_APP_PRIVATE_KEY }}

- name: Get GitHub App User Details
id: app-user
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
user_name="${{ steps.app-token.outputs.app-slug }}[bot]"
user_id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)
{
echo "user-name=${user_name}"
echo "user-id=${user_id}"
echo "email=${user_id}+${user_name}@users.noreply.github.com"
} >> "$GITHUB_OUTPUT"
# Note: We can't change the head branch once a PR is opened. Thus we need to delete any branches
# that exist from any existing open pull requests.
# that exist from any existing open pull requests. (App Perm = Pull Request: Read + Write)
- name: Close existing release pull requests
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const prTitleRegex = /^chore\(release\): v\d+\.\d+\.\d+$/;
Expand All @@ -122,7 +113,7 @@ jobs:
console.log('Analyzing PR', pr.number, pr.title, pr.user.login);
// Check if the title matches the format and it's created by the correct user
if (prTitleRegex.test(pr.title) && pr.user.login === 'github-actions[bot]') {
if (prTitleRegex.test(pr.title) && pr.user.login === '${{ steps.app-user.outputs.user-name }}') {
console.log(`PR #${pr.number} has a valid title: ${pr.title}`);
// Close the existing pull request
Expand All @@ -145,47 +136,16 @@ jobs:
}
}
# Additional caveat:
# When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN,
# with the exception of workflow_dispatch and repository_dispatch, will not create a new workflow run.
# This prevents you from accidentally creating recursive workflow runs.
#
# There is no way to even trigger this with a repository_dispatch. Therefore, currently the only way
# is to use a separate PAT. In the future we could release a bot to help automate a lot of this.
#
# https://github.com/orgs/community/discussions/65321
- name: Create new pull request
uses: actions/github-script@v7
id: pull-request
- name: Create Branch and Pull Request
uses: peter-evans/create-pull-request@v7
with:
github-token: ${{ secrets.GH_TOKEN_RELEASE_AUTOMATION }}
script: |
const version = '${{ env.VERSION }}';
const prTitle = `chore(release): v${version}`;
const branchName = `release-v${version}`;
const changelog = ${{ steps.changelog.outputs.result }};
const prCreateData = {
owner: context.repo.owner,
repo: context.repo.repo,
title: prTitle,
head: '${{ env.BRANCH_NAME }}',
base: 'main',
body: changelog,
};
console.log('Creating new PR. Context:');
console.dir(prCreateData);
const { data: pr } = await github.rest.pulls.create(prCreateData);
console.log(`Created new PR #${pr.number}`);
// Add labels if they don't exist
console.log('Creating PR labels')
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['release']
});
return pr.number;
token: ${{ steps.app-token.outputs.token }}
base: main
branch: ${{ env.BRANCH_NAME }}
title: "chore(release): v${{ env.VERSION }}"
body: ${{ fromJSON(steps.changelog.outputs.result) }}
commit-message: "chore(release): v${{ env.VERSION }}"
sign-commits: true # Note: When setting sign-commits: true the action will ignore the committer and author inputs.
delete-branch: true
labels: release
signoff: true
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
if: |
startsWith(github.event.pull_request.title, 'chore(release):') &&
contains(github.event.pull_request.body, '<!-- RELEASE-NOTES-MARKER-START -->') &&
github.event.pull_request.user.login == 'github-actions[bot]' &&
github.event.pull_request.user.login == 'release-preview[bot]' &&
github.event.pull_request.merged == true
steps:
Expand Down

0 comments on commit 53d079e

Please sign in to comment.