diff --git a/.github/workflows/issue-to-draft-pr.yml b/.github/workflows/issue-to-draft-pr.yml new file mode 100644 index 000000000000..ae731a56a706 --- /dev/null +++ b/.github/workflows/issue-to-draft-pr.yml @@ -0,0 +1,123 @@ +name: Issue to draft PR + +on: + issues: + types: [opened] + workflow_dispatch: + +permissions: {} + +jobs: + create-draft-pr: + permissions: + contents: write + issues: write + pull-requests: write + runs-on: blacksmith-16vcpu-ubuntu-2404 + steps: + - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 + id: app-token + continue-on-error: true + with: + app-id: "2729701" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 + id: app-token-fallback + if: steps.app-token.outcome == 'failure' + with: + app-id: "2971289" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }} + + - name: Create issue branch and draft PR + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }} + script: | + const issue = context.payload.issue; + if (!issue) { + core.info("No issue payload found, skipping."); + return; + } + + const owner = context.repo.owner; + const repo = context.repo.repo; + const issueNumber = issue.number; + const issueTitle = (issue.title ?? `Issue ${issueNumber}`).trim(); + const issueBody = (issue.body ?? "").trim() || "_No description provided._"; + const issueUrl = issue.html_url; + const branchName = `issue-${issueNumber}`; + const prTitle = `[Issue #${issueNumber}] ${issueTitle}`; + + const existingPrs = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branchName}`, + state: "all", + per_page: 1, + }); + + if (existingPrs.data.length > 0) { + core.info(`PR already exists for ${branchName}, skipping creation.`); + return; + } + + let branchExists = true; + try { + await github.rest.repos.getBranch({ + owner, + repo, + branch: branchName, + }); + } catch (error) { + if (error.status === 404) { + branchExists = false; + } else { + throw error; + } + } + + if (!branchExists) { + const mainBranch = await github.rest.repos.getBranch({ + owner, + repo, + branch: "main", + }); + + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${branchName}`, + sha: mainBranch.data.commit.sha, + }); + } + + const prBody = [ + "## Source issue", + `- #${issueNumber}`, + `- ${issueUrl}`, + "", + "## Original issue description", + issueBody, + "", + `Related to #${issueNumber}`, + ].join("\n"); + + const createdPr = await github.rest.pulls.create({ + owner, + repo, + title: prTitle, + head: branchName, + base: "main", + body: prBody, + draft: true, + }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: `Opened draft PR #${createdPr.data.number}: ${createdPr.data.html_url}`, + }); + + core.info(`Created draft PR #${createdPr.data.number} for issue #${issueNumber}.`);