From f9643ab935bc5f88ce82ec1ddebbaf3f9520f9fa Mon Sep 17 00:00:00 2001 From: Joel Butcher Date: Wed, 6 Dec 2023 15:53:55 +0000 Subject: [PATCH] add files to check issues for reproduction repository and maintainer access --- .../workflows/check-pr-maintainer-access.yml | 67 ++++++ .github/workflows/manage-issue.yml | 197 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 .github/workflows/check-pr-maintainer-access.yml create mode 100644 .github/workflows/manage-issue.yml diff --git a/.github/workflows/check-pr-maintainer-access.yml b/.github/workflows/check-pr-maintainer-access.yml new file mode 100644 index 00000000..45dec275 --- /dev/null +++ b/.github/workflows/check-pr-maintainer-access.yml @@ -0,0 +1,67 @@ +name: check-pr-maintainer-access + +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + notify-when-maintainers-cannot-edit: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v3 + with: + script: | + const query = ` + query($number: Int!) { + repository(owner: "joelbutcher", name: "socialstream") { + pullRequest(number: $number) { + headRepositoryOwner { + login + } + maintainerCanModify + } + } + } + ` + + const pullNumber = context.issue.number + const variables = { number: pullNumber } + + try { + console.log(`Check #${pullNumber} for maintainer edit access...`) + const result = await github.graphql(query, variables) + + console.log(JSON.stringify(result, null, 2)) + + const pullRequest = result.repository.pullRequest + + if (pullRequest.headRepositoryOwner.login === 'joelbutcher') { + console.log('PR owned by joelbutcher') + + return + } + + if (! pullRequest.maintainerCanModify) { + console.log('PR not owned by joelbutcher and does not have maintainer edits enabled') + + await github.issues.createComment({ + issue_number: pullNumber, + owner: 'joelbutcher', + repo: 'socialstream', + body: 'Thanks for submitting a PR!\n\nIn order to review and merge PRs most efficiently, we require that all PRs grant maintainer edit access before we review them. If your fork belongs to a GitHub organization, please move the repository to your personal account and try again. If you\'re already using a personal fork, you can learn how to enable maintainer access [in the GitHub documentation](https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).' + }) + + await github.issues.update({ + issue_number: pullNumber, + owner: 'joelbutcher', + repo: context.repo.repo, + state: 'closed' + }) + } + } catch(error) { + console.log(error) + } diff --git a/.github/workflows/manage-issue.yml b/.github/workflows/manage-issue.yml new file mode 100644 index 00000000..46a27384 --- /dev/null +++ b/.github/workflows/manage-issue.yml @@ -0,0 +1,197 @@ +name: manage-issue + +on: + issues: + types: [opened, edited] + +jobs: + check-repro: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v3 + with: + script: | + const URL_REGEXP = /### Reproduction repository[\r\n]+([^#]+)###/m + const REPRO_STEPS_REGEXP = /### Steps to reproduce[\r\n]+([^#]+)###/m + const LABEL_NEEDS_MORE_INFORMATION = 'needs more info' + const LABEL_UNCONFIRMED = 'unconfirmed' + + function debug(...args) { + core.info(args.map(JSON.stringify).join(' ')) + } + + if (context.payload.comment) { + debug('Ignoring comment update.') + + return + } + + const user = context.payload.sender.login + const issue = context.payload.issue + const body = issue.body + const urlMatch = body.match(URL_REGEXP) + const reproStepsMatch = body.match(REPRO_STEPS_REGEXP) + const url = urlMatch !== null ? urlMatch[1].trim() : null + const reproSteps = reproStepsMatch !== null ? reproStepsMatch[1].trim() : null + + debug(`Found URL '${url}'`) + debug(`Found repro steps '${reproSteps}'`) + + async function createComment(comment) { + comment = comment + .split('\n') + .map((line) => line.trim()) + .join('\n') + .trim() + + await github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment, + }) + } + + async function getGitHubActionComments() { + debug(`Loading existing comments...`) + + const comments = await github.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }) + + return comments.data.filter(comment => { + debug(`comment by user: '${comment.user.login}'`) + return comment.user.login === 'github-actions[bot]' + }) + } + + async function getIssueLabels() { + const issues = await github.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }) + + return issues.data + } + + async function updateIssue(state, state_reason = null) { + await github.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state, + state_reason, + }) + } + + async function closeWithComment(comment) { + if (issue.state !== 'open') { + debug(`Issue is not open`) + + return + } + + const comments = await getGitHubActionComments() + + if (comments.length > 0) { + debug(`Already commented on issue won't comment again`) + + return + } + + debug(`Missing required information`) + + await github.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [LABEL_NEEDS_MORE_INFORMATION], + }) + + await createComment(comment) + + await updateIssue('closed', 'not_planned') + } + + async function openWithComment(comment) { + if (issue.state !== 'closed') { + debug(`Issue is already open`) + + return + } + + const labels = await getIssueLabels() + const label = labels.find(label => label.name === LABEL_NEEDS_MORE_INFORMATION) + + if (! label) { + debug(`Issue was not tagged as needs information`) + + return + } + + const comments = await getGitHubActionComments() + + if (comments.length === 0) { + debug(`Issue was closed by someone else, won't reopen`) + + return + } + + debug(`Reopening closed issue`) + + await github.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL_NEEDS_MORE_INFORMATION, + }) + + await createComment(comment) + + await updateIssue('open') + } + + const COMMENT_HEADER = ` + Hey @${user}! We're sorry to hear that you've hit this issue. 💛 + `.trim() + + const NO_REPRO_URL = ((! url) || (! url.includes('https://github.com/')) || (url.includes('https://github.com/joelbutcher') && (! url.includes('https://github.com/joelbutcher/socialstream-demo')))) + const NO_REPRO_STEPS = reproSteps.length < 25 + + if (NO_REPRO_URL || NO_REPRO_STEPS) { + let comment = ` + ${COMMENT_HEADER} + + ` + + if (NO_REPRO_URL) { + comment += ` + However, it looks like you forgot to fill in the reproduction repository URL. Can you edit your original post and then we'll look at your issue? + + We need a public GitHub repository which contains a Laravel app with the minimal amount of Socialstream code to reproduce the problem. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn\'t matter if your real project is private / confidential, since we want a link to a separate, isolated reproduction. That would allow us to download it and review your bug much easier, so it can be fixed quicker. Please make sure to include a database seeder with everything we need to set the app up quickly. + ` + } + + if (NO_REPRO_URL && NO_REPRO_STEPS) { + comment += ` + + Also, ` + } else if (NO_REPRO_STEPS) { + comment += ` + + However, ` + } + + if (NO_REPRO_STEPS) { + comment += `it doesn't look like you've provided much information on how to replicate the issue. Please edit your original post with clear steps we need to take.` + } + + closeWithComment(comment) + } else { + openWithComment(` + Thank you for providing reproduction steps! Reopening the issue now. + `) + }