diff --git a/.github/scripts/changelog.js b/.github/scripts/changelog.js index 8eaee92..5285895 100644 --- a/.github/scripts/changelog.js +++ b/.github/scripts/changelog.js @@ -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} A promise that resolves after the specified time has passed. + */ +function sleep(ms) { + new Promise((resolve) => setTimeout(resolve, ms)); +} + /** * Validates required environment variables */ @@ -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} - 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. @@ -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('.github.com')) { + 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) { @@ -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) { diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index 2fd95b0..b7087ff 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -6,8 +6,6 @@ on: pull_request: branches: - main - repository_dispatch: - types: [release-preview] permissions: contents: read diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 085cb01..caa54e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8d27735..c954668 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,8 +7,6 @@ on: push: branches: - main - repository_dispatch: - types: [release-preview] schedule: - cron: "31 7 * * 3" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2e98ff9..6461b6d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,13 +7,10 @@ on: push: branches: - main - repository_dispatch: - types: [release-preview] permissions: contents: read packages: read - statuses: write jobs: lint: diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index 0937445..4289544 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -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: @@ -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 @@ -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, @@ -150,6 +145,26 @@ 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, @@ -157,7 +172,6 @@ jobs: head: '${{ env.BRANCH_NAME }}', base: 'main', body: changelog, - labels: ['release'] }; console.log('Creating new PR. Context:'); console.dir(prCreateData); @@ -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;