Skip to content

fix: add retry logic to username fetching and improve dispatching #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions .github/scripts/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,22 @@ Important formatting rules:
- 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.
- Omit sections with no content silently - do not add any notes or explanations about omitted sections.
`;

// In-memory cache for username lookups
const usernameCache = new Map();

/**
* Pauses execution for a specified amount of time.
*
* @param {number} ms - The number of milliseconds to sleep.
* @returns {Promise<void>} A promise that resolves after the specified time has passed.
*/
function sleep(ms) {
new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* Validates required environment variables
*/
Expand Down Expand Up @@ -120,6 +130,25 @@ function githubApiRequest(path) {
});
}

/**
* Makes a request to the GitHub API with retries
* @param {string} path - The API endpoint path including query parameters
* @param {number} retries - Number of retries remaining
* @returns {Promise<object|null>} - Parsed JSON response or null for 404s
*/
async function githubApiRequestWithRetry(path, retries = 2) {
try {
return await githubApiRequest(path);
} catch (error) {
if (retries > 0 && error.message.includes('403')) {
console.log(`Rate limited, retrying after 2 seconds... (${retries} retries left)`);
await sleep(2000);
return githubApiRequestWithRetry(path, retries - 1);
}
throw error;
}
}

/**
* Attempts to resolve a GitHub username from a commit email address
* using multiple GitHub API endpoints.
Expand All @@ -129,10 +158,44 @@ function githubApiRequest(path) {
*/
async function resolveGitHubUsername(commitEmail) {
console.log('Attempting to resolve username:', commitEmail);

// Local resolution - Handle various GitHub email patterns
const emailMatches = email.match(/^(?:(?:[^@]+)?@)?([^@]+)$/);
if (emailMatches) {
const [, domain] = emailMatches;

// Handle github.com email variations
if (domain === 'users.noreply.github.com') {
// Extract username from 1234567+username@users.noreply.github.com
// or username@users.noreply.github.com
const matches = email.match(/^(?:(\d+)\+)?([^@]+)@users\.noreply\.github\.com$/);
return matches ? matches[2] : null;
}

// Handle organization emails like username@organization.github.com
if (domain.endsWith('.gh.loli.garden')) {
const matches = email.match(/^([^@]+)@[^@]+\.github\.com$/);
return matches ? matches[1] : null;
}

// Handle GitHub Enterprise emails
// Pattern: username@github.{enterprise}.com
const enterpriseMatches = email.match(/^([^@]+)@github\.[^@]+\.com$/);
if (enterpriseMatches) {
return enterpriseMatches[1];
}

// Handle GitHub staff emails
if (email.endsWith('@github.com')) {
const matches = email.match(/^([^@]+)@github\.com$/);
return matches ? matches[1] : null;
}
}

try {
// First attempt: Direct API search for user by email
console.log(`[${commitEmail}] Querying user API`);
const searchResponse = await githubApiRequest(
const searchResponse = await githubApiRequestWithRetry(
`https://api.github.com/search/users?q=${encodeURIComponent(commitEmail)}+in:email`,
);
if (searchResponse?.items && searchResponse.items.length > 0) {
Expand All @@ -148,7 +211,7 @@ async function resolveGitHubUsername(commitEmail) {
try {
console.log(`[${commitEmail}] Querying commit API`);
// Second attempt: Check commit API for associated username
const commitSearchResponse = await githubApiRequest(
const commitSearchResponse = await githubApiRequestWithRetry(
`https://api.github.com/search/commits?q=author-email:${encodeURIComponent(commitEmail)}&per_page=25`,
);
if (commitSearchResponse?.items?.length > 0) {
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/check-dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ on:
pull_request:
branches:
- main
repository_dispatch:
types: [release-preview]

permissions:
contents: read
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ on:
types: [opened, reopened, synchronize]
branches:
- main
repository_dispatch:
types: [release-preview]
workflow_dispatch:

permissions:
contents: write # Required to create tags, creaste releases and update wiki
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ on:
push:
branches:
- main
repository_dispatch:
types: [release-preview]
schedule:
- cron: "31 7 * * 3"

Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@ on:
push:
branches:
- main
repository_dispatch:
types: [release-preview]

permissions:
contents: read
packages: read
statuses: write

jobs:
lint:
Expand Down
38 changes: 27 additions & 11 deletions .github/workflows/release-start.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
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
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -43,7 +44,6 @@ jobs:
# 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
Expand Down Expand Up @@ -101,19 +101,14 @@ jobs:
core.setFailed(error.message);
}

- name: Create new PR
# 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.
- name: Close existing release pull requests
uses: actions/github-script@v7
with:
script: |
const version = '${{ env.VERSION }}';
const prTitle = `chore(release): v${version}`;
const branchName = `release-v${version}`;
const changelog = ${{ steps.changelog.outputs.result }};
const prTitleRegex = /^chore\(release\): v\d+\.\d+\.\d+$/;

// 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.

console.log('Searching for existing open PRs ...');
const { data: existingPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
Expand Down Expand Up @@ -150,14 +145,33 @@ 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
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,
labels: ['release']
};
console.log('Creating new PR. Context:');
console.dir(prCreateData);
Expand All @@ -166,10 +180,12 @@ jobs:
console.log(`Created new PR #${pr.number}`);

// Add labels if they don't exist
console.log('Updating PR labels')
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;