From 2efafb368b7f30151ceb5b5b90baa1a56833fca8 Mon Sep 17 00:00:00 2001 From: Enrique Velez <145390508+enlike21@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:15:47 +0200 Subject: [PATCH] Assign issues to the author of the PR that closes them A new workflow assigns each issue a PR closes ("Closes #12" or a link from the PR sidebar) to whoever opened the PR. GitHub won't assign someone without write access who hasn't taken part in the issue, so in that case it asks them on the issue to leave a comment, and assigns them when they do. It uses pull_request_target so it works for PRs from forks, and never checks out the PR's code. The PR template suggested "Cierra #123", which GitHub doesn't recognise (#26 never got linked to #10); it now says "Closes #123". --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/assign-issue.yml | 105 +++++++++++++++++++++++++++++ CONTRIBUTING.md | 4 ++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/assign-issue.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aec042a..8fe0e1b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ## Qué cambia - + ## Cómo probarlo diff --git a/.github/workflows/assign-issue.yml b/.github/workflows/assign-issue.yml new file mode 100644 index 0000000..935890c --- /dev/null +++ b/.github/workflows/assign-issue.yml @@ -0,0 +1,105 @@ +name: Assign issue to PR author + +# When a pull request closes an issue ("Closes #12" in its description, or linked from the +# PR sidebar), assign the issue to whoever opened the PR, so the issue list shows who is +# working on what. +# +# GitHub only lets us assign people with write access or who have taken part in the issue. +# For a first-time contributor who hasn't, we ask them on the issue to leave a comment, and +# the issue_comment trigger assigns them as soon as they do. +# +# pull_request_target runs with this repo's token even for PRs from forks. That is safe here +# because the job never checks out or runs the PR's code: it only reads PR metadata. + +on: + pull_request_target: + types: [opened, reopened, edited, ready_for_review] + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: read + +jobs: + assign: + # Comments on pull requests also fire issue_comment; only issue comments matter here. + if: github.event_name == 'pull_request_target' || !github.event.issue.pull_request + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const MARKER = ''; + + // Issues of this repo that the PR will close on merge. + const closingIssues = async (prNumber) => { + const { repository } = await github.graphql(` + query($owner: String!, $repo: String!, $pr: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + closingIssuesReferences(first: 10) { + nodes { number repository { nameWithOwner } } + } + } + } + }`, { owner, repo, pr: prNumber }); + return repository.pullRequest.closingIssuesReferences.nodes + .filter((issue) => issue.repository.nameWithOwner === `${owner}/${repo}`) + .map((issue) => issue.number); + }; + + // GitHub silently drops assignees it doesn't allow, so check the result. + const assign = async (issueNumber, login) => { + const { data } = await github.rest.issues.addAssignees({ + owner, repo, issue_number: issueNumber, assignees: [login], + }); + return data.assignees.some((a) => a.login === login); + }; + + if (context.eventName === 'pull_request_target') { + const pr = context.payload.pull_request; + const login = pr.user.login; + if (pr.user.type === 'Bot') return; + + for (const number of await closingIssues(pr.number)) { + const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: number }); + if (issue.state !== 'open' || issue.assignees.some((a) => a.login === login)) continue; + + if (await assign(number, login)) { + core.info(`Assigned #${number} to @${login}`); + continue; + } + + // Not allowed yet: ask them to comment on the issue, only once. + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: number, per_page: 100, + }); + if (comments.some((c) => c.body.includes(MARKER) && c.body.includes(`@${login}`))) continue; + await github.rest.issues.createComment({ + owner, repo, issue_number: number, + body: `${MARKER}\n@${login}, gracias por abrir #${pr.number} para esto 🙌 ` + + 'GitHub solo deja asignar un issue a quien ha participado en él: ' + + 'deja cualquier comentario aquí y te lo asigno automáticamente.', + }); + core.info(`Asked @${login} to comment on #${number}`); + } + return; + } + + // issue_comment: assign the commenter if one of their open PRs closes this issue. + const issue = context.payload.issue; + const login = context.payload.comment.user.login; + if (context.payload.comment.user.type === 'Bot' || issue.state !== 'open') return; + if (issue.assignees.some((a) => a.login === login)) return; + + const pulls = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + }); + for (const pr of pulls.filter((p) => p.user.login === login)) { + if ((await closingIssues(pr.number)).includes(issue.number)) { + if (await assign(issue.number, login)) core.info(`Assigned #${issue.number} to @${login}`); + return; + } + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32efb41..4c6550d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,6 +90,10 @@ y de la plantilla `templates/game/index.html.twig`. ## Pull requests - Una cosa por PR, con una descripción de qué cambia y por qué (la plantilla te guía). +- Si el PR resuelve un issue, escribe `Closes #123` en la descripción (en inglés: GitHub no + entiende «Cierra»). Así el issue se cierra solo al fusionar y se te asigna automáticamente. + Si es tu primera vez en ese issue, un bot te pedirá que dejes un comentario en él, porque + GitHub solo deja asignar issues a quien ha participado en ellos. - Añade o actualiza tests cuando cambie el comportamiento. - Los mensajes de commit pueden ir en español o en inglés; que expliquen el porqué. - La CI (estilo, PHPStan y tests) tiene que pasar, y un mantenedor revisará el PR antes de