chore(deps): drop unused content-disposition dependency #178
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| name: PR assignment | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, closed] | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| # All three behaviors live as steps under one job so the PR Checks | |
| # tab shows a single entry per event instead of two-or-three skipped | |
| # siblings. Step-level if-guards keep the actual work scoped. | |
| pr-assignment: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Self-assign PR author to the PR | |
| if: >- | |
| github.event.action == 'opened' | |
| && github.event.pull_request.user.type != 'Bot' | |
| && github.event.pull_request.assignees[0] == null | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| core.info(`PR #${pr.number} opened by ${pr.user.login}; self-assigning.`); | |
| try { | |
| await github.rest.issues.addAssignees({ | |
| ...context.repo, | |
| issue_number: pr.number, | |
| assignees: [pr.user.login], | |
| }); | |
| core.info(`Assigned ${pr.user.login} to PR #${pr.number}`); | |
| } catch (e) { | |
| core.warning(`Self-assign on PR #${pr.number} failed: ${e.message}`); | |
| } | |
| - name: Sync PR opener as assignee on linked issues | |
| # Mirror the PR opener as an assignee on each same-repo issue listed | |
| # in closingIssuesReferences. On body edits, also drop the opener | |
| # from issues whose closing keyword was removed. Other manual | |
| # assignees are never touched here, so this never fights with the | |
| # merge-time credit step or human triage decisions. | |
| if: >- | |
| contains(fromJSON('["opened","edited"]'), github.event.action) | |
| && github.event.pull_request.state == 'open' | |
| && github.event.pull_request.user.type != 'Bot' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const pr = context.payload.pull_request; | |
| const opener = pr.user.login; | |
| core.info(`Event ${context.payload.action} on PR #${pr.number} by ${opener}; syncing closing-issue assignees.`); | |
| const { repository: { pullRequest: prq } } = await github.graphql(` | |
| query($owner: String!, $repo: String!, $pr: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $pr) { | |
| closingIssuesReferences(first: 50) { | |
| nodes { number repository { nameWithOwner } } | |
| } | |
| } | |
| } | |
| }`, { owner, repo, pr: pr.number }); | |
| const sameRepo = `${owner}/${repo}`; | |
| const allRefs = prq.closingIssuesReferences.nodes; | |
| const linked = allRefs | |
| .filter((n) => n.repository.nameWithOwner === sameRepo) | |
| .map((n) => n.number); | |
| const crossRepo = allRefs.filter((n) => n.repository.nameWithOwner !== sameRepo); | |
| core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`); | |
| if (crossRepo.length) { | |
| core.info(`Skipping ${crossRepo.length} cross-repo reference(s): ${crossRepo.map((n) => `${n.repository.nameWithOwner}#${n.number}`).join(', ')}`); | |
| } | |
| for (const issue_number of linked) { | |
| try { | |
| await github.rest.issues.addAssignees({ | |
| owner, repo, issue_number, assignees: [opener], | |
| }); | |
| core.info(`Assigned ${opener} to issue #${issue_number}`); | |
| } catch (e) { | |
| core.warning(`addAssignees on #${issue_number} failed: ${e.message}`); | |
| } | |
| } | |
| // On body edit, find closing refs that disappeared from the body | |
| // and remove the opener from those issues. closingIssuesReferences | |
| // is a snapshot of the *new* state, so we need text-diff to detect | |
| // removals. Cross-repo refs are intentionally skipped. | |
| if ( | |
| context.payload.action === 'edited' && | |
| context.payload.changes && | |
| context.payload.changes.body && | |
| typeof context.payload.changes.body.from === 'string' | |
| ) { | |
| // GitHub also recognizes the colon form ("Closes: #123"), so | |
| // allow an optional ":" between the keyword and the issue | |
| // ref. Multiple refs on one line still need their own | |
| // keyword each ("Closes #1, closes #2"), matching the | |
| // `closingIssuesReferences` semantics — `Closes #1, #2` | |
| // links only #1, so the diff treats only #1 as removed. | |
| const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+#(\d+)/gi; | |
| const oldBody = context.payload.changes.body.from || ''; | |
| const newBody = pr.body || ''; | |
| const oldRefs = new Set([...oldBody.matchAll(re)].map((m) => Number(m[1]))); | |
| const newRefs = new Set([...newBody.matchAll(re)].map((m) => Number(m[1]))); | |
| const removed = [...oldRefs].filter((n) => !newRefs.has(n)); | |
| core.info(`Body-diff: oldRefs=[${[...oldRefs].join(',')}] newRefs=[${[...newRefs].join(',')}] removed=[${removed.join(',')}]`); | |
| for (const issue_number of removed) { | |
| try { | |
| await github.rest.issues.removeAssignees({ | |
| owner, repo, issue_number, assignees: [opener], | |
| }); | |
| core.info(`Unassigned ${opener} from issue #${issue_number}`); | |
| } catch (e) { | |
| core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`); | |
| } | |
| } | |
| } else if (context.payload.action === 'edited') { | |
| core.info(`Body unchanged on edit; skipping removal-detection.`); | |
| } | |
| - name: Unassign PR opener from linked issues on PR close without merge | |
| # When a PR is closed without merging, the opener was added to | |
| # linked issues by the "Sync PR opener" step on open/edit. Mirror | |
| # the close: remove them so abandoned PRs do not leave stale | |
| # assignees. With no other assignee the issue then drops back into | |
| # the `is:open no:assignee` triage filter automatically. Cross-repo | |
| # refs are skipped, consistent with the assign side. | |
| if: >- | |
| github.event.action == 'closed' | |
| && github.event.pull_request.merged == false | |
| && github.event.pull_request.user.type != 'Bot' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const pr = context.payload.pull_request; | |
| const opener = pr.user.login; | |
| core.info(`PR #${pr.number} closed without merge; unassigning ${opener} from linked issues.`); | |
| const { repository: { pullRequest: prq } } = await github.graphql(` | |
| query($owner: String!, $repo: String!, $pr: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $pr) { | |
| closingIssuesReferences(first: 50) { | |
| nodes { number repository { nameWithOwner } } | |
| } | |
| } | |
| } | |
| }`, { owner, repo, pr: pr.number }); | |
| const sameRepo = `${owner}/${repo}`; | |
| const linked = prq.closingIssuesReferences.nodes | |
| .filter((n) => n.repository.nameWithOwner === sameRepo) | |
| .map((n) => n.number); | |
| core.info(`Found ${linked.length} same-repo closing reference(s): ${linked.join(', ') || '(none)'}`); | |
| for (const issue_number of linked) { | |
| try { | |
| await github.rest.issues.removeAssignees({ | |
| owner, repo, issue_number, assignees: [opener], | |
| }); | |
| core.info(`Unassigned ${opener} from issue #${issue_number}`); | |
| } catch (e) { | |
| core.warning(`removeAssignees on #${issue_number} failed: ${e.message}`); | |
| } | |
| } | |
| - name: Credit issue assignees on PR merge | |
| if: github.event.action == 'closed' && github.event.pull_request.merged | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const pr = context.payload.pull_request; | |
| const opener = pr.user; | |
| const isHuman = (l) => l && !l.endsWith('[bot]'); | |
| core.info(`PR #${pr.number} merged by opener=${opener.login}; computing credited authors.`); | |
| const { repository: { pullRequest: prq } } = await github.graphql(` | |
| query($owner: String!, $repo: String!, $pr: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $pr) { | |
| closingIssuesReferences(first: 50) { | |
| nodes { | |
| number | |
| repository { nameWithOwner } | |
| assignees(first: 20) { nodes { login } } | |
| } | |
| } | |
| commits(first: 250) { | |
| nodes { commit { | |
| parents { totalCount } | |
| authors(first: 10) { nodes { user { login } } } | |
| } } | |
| } | |
| } | |
| } | |
| }`, { owner, repo, pr: pr.number }); | |
| const authors = new Set(); | |
| if (opener.type !== 'Bot' && isHuman(opener.login)) authors.add(opener.login); | |
| for (const { commit } of prq.commits.nodes) { | |
| if (commit.parents.totalCount > 1) continue; | |
| for (const a of commit.authors.nodes) { | |
| if (isHuman(a.user?.login)) authors.add(a.user.login); | |
| } | |
| } | |
| const credited = [...authors].slice(0, 10); | |
| const creditedSet = new Set(credited); | |
| core.info(`Credited authors (max 10, [bot] filtered): [${credited.join(', ') || '(none)'}]`); | |
| if (!credited.length) { | |
| core.info(`No human authors to credit; skipping all linked issues.`); | |
| return; | |
| } | |
| const sameRepoIssues = prq.closingIssuesReferences.nodes.filter((n) => n.repository.nameWithOwner === `${owner}/${repo}`); | |
| core.info(`Linked same-repo issues to credit: [${sameRepoIssues.map((i) => `#${i.number}`).join(', ') || '(none)'}]`); | |
| for (const issue of sameRepoIssues) { | |
| const current = issue.assignees.nodes.map(n => n.login); | |
| const toRemove = current.filter(l => !creditedSet.has(l)); | |
| const toAdd = credited.filter(l => !current.includes(l)); | |
| const args = { owner, repo, issue_number: issue.number }; | |
| core.info(`Issue #${issue.number}: current=[${current.join(',')}] credited=[${credited.join(',')}] toRemove=[${toRemove.join(',')}] toAdd=[${toAdd.join(',')}]`); | |
| try { | |
| if (toRemove.length) { | |
| await github.rest.issues.removeAssignees({ ...args, assignees: toRemove }); | |
| core.info(`Removed [${toRemove.join(', ')}] from issue #${issue.number}`); | |
| } | |
| if (toAdd.length) { | |
| await github.rest.issues.addAssignees({ ...args, assignees: toAdd }); | |
| core.info(`Added [${toAdd.join(', ')}] to issue #${issue.number}`); | |
| } | |
| } catch (e) { | |
| core.warning(`Updating assignees on #${issue.number} failed: ${e.message}`); | |
| } | |
| } |