Issue: $USER closed this in #$PR $DURATION ago

How do I get $PR via the API?

If not possible: How do I get $PR via GraphQL?

1 Like

IMAO it’s pretty ashaming for a ReST API to have to do something like this:

From bbfb2c16cccd9ef8ff5880b0de4029874e6c4119 Mon Sep 17 00:00:00 2001
From: "Alexander A. Klimov" <alexander.klimov@icinga.com>
Date: Tue, 8 Sep 2020 10:18:15 +0200
Subject: [PATCH] Add issues to their PRs' milestones

---
 README.md                      |  5 +++
 package.json                   |  6 +++
 src/index.ts                   |  5 +++
 src/issue-milestone-from-pr.ts | 82 ++++++++++++++++++++++++++++++++++
 tsconfig.json                  |  3 +-
 5 files changed, 100 insertions(+), 1 deletion(-)
 create mode 100644 src/issue-milestone-from-pr.ts

diff --git a/README.md b/README.md
index 76c3fcb..a58e771 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,11 @@ docker run --rm -d \

 ## Actions

+### Adds issues to milestones
+
+Automatically sets issues' milestones
+based on the pull requests which closed them.
+
 ### Adds pull requests to milestones

 Automatically sets PRs' milestones based on the target branch.
diff --git a/package.json b/package.json
index 3114523..e3a7692 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,12 @@
     "test": "jest"
   },
   "dependencies": {
+    "@types/jsdom": "^16.2.3",
+    "@types/jsdom-global": "^3.0.2",
+    "@types/node-fetch": "^2.5.7",
+    "jsdom": "^16.4.0",
+    "jsdom-global": "^3.0.2",
+    "node-fetch": "^2.6.0",
     "probot": "10.1.1"
   },
   "devDependencies": {
diff --git a/src/index.ts b/src/index.ts
index 3014527..7e52be0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,15 @@
 // Icinga Probot | (c) 2020 Icinga GmbH | GPLv2+

+import jsdomGlobal from 'jsdom-global'
 import { Application } from 'probot'
+import { issueMilestoneFromPr } from './issue-milestone-from-pr'
 import { prAutoMilestone } from './pr-auto-milestone'
 import { prClosedMilestone } from './pr-closed-milestone'

+jsdomGlobal()
+
 export = (app: Application) => {
+    app.on('issues.closed', issueMilestoneFromPr)
     app.on('pull_request.opened', prAutoMilestone)
     app.on('pull_request.closed', prClosedMilestone)
 }
diff --git a/src/issue-milestone-from-pr.ts b/src/issue-milestone-from-pr.ts
new file mode 100644
index 0000000..763e676
--- /dev/null
+++ b/src/issue-milestone-from-pr.ts
@@ -0,0 +1,82 @@
+// Icinga Probot | (c) 2020 Icinga GmbH | GPLv2+
+
+import { JSDOM } from 'jsdom'
+import fetch from 'node-fetch'
+import { Context } from 'probot'
+
+export async function issueMilestoneFromPr(context: Context) {
+    const issue = context.issue()
+    const url = 'https://github.com/' + issue.owner + '/' + issue.repo + '/issues/' + issue.issue_number
+    const response = await fetch(url)
+
+    if (!response.ok) {
+        throw 'Got ' + response.status + ' ' + JSON.stringify(response.statusText) + ' from ' + JSON.stringify(url)
+    }
+
+    // TODO: Replace with something stable
+    // https://github.community/t/issue-user-closed-this-in-pr-duration-ago/129478
+
+    const document = (new JSDOM(await response.buffer())).window.document
+
+    const hiddenItems = Array.from(document.querySelectorAll('button'))
+        .filter(button => button.textContent !== null && /\bhidden items\b/.test(button.textContent))
+
+    if (hiddenItems.length) {
+        await context.github.issues.createComment({
+            ...issue,
+            body: "Someone closed this issue just now, but due to " +
+                "[a missing feature](https://github.community/t/issue-user-closed-this-in-pr-duration-ago/129478) " +
+                "in the GitHub API and the high amount of comments here I can't figure out " +
+                "whether this issue was closed due to a PR merge. " +
+                "Please check by yourself whether this issue is on the correct milestone."
+        })
+
+        return
+    }
+
+    const timeline = document.querySelectorAll('div.TimelineItem-body')
+    let lastClose: Element | null = null
+
+    for (let i = 0; i < timeline.length; ++i) {
+        const item = timeline[i]
+
+        if (item.textContent !== null && /\bclosed this\b/.test(item.textContent)) {
+            lastClose = item
+        }
+    }
+
+    if (lastClose === null) {
+        throw 'Issue #' + issue.issue_number + ' has never been closed'
+    }
+
+    const prLink = lastClose.querySelector('a[data-hovercard-type="pull_request"]')
+
+    if (prLink === null) {
+        // Issue was closed not by a PR
+        return
+    }
+
+    const href = prLink.attributes.getNamedItem('href')
+
+    if (href === null) {
+        throw 'Issue #' + issue.issue_number + ": last closed-by-PR event's PR link has no href"
+    }
+
+    // ".../pull/42" => "42"
+    const match = /\/(\d+)$/.exec(href.value)
+
+    if (match === null) {
+        throw 'Issue #' + issue.issue_number + ": bad last closed-by-PR event's PR link's href"
+    }
+
+    const milestone = (await context.github.pulls.get({
+        owner: issue.owner, repo: issue.repo, pull_number: Number(match[1])
+    })).data.milestone
+
+    if (milestone === null) {
+        // Closing PR is not on any milestone
+        return
+    }
+
+    await context.github.issues.update({...issue, milestone: milestone.number})
+}
diff --git a/tsconfig.json b/tsconfig.json
index 4425e72..12d71dd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,8 @@
     "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
     "lib": [
       "es2015",
-      "es2017"
+      "es2017",
+      "dom"
     ] /* Specify library files to be included in the compilation. */,
     "allowJs": true /* Allow javascript files to be compiled. */,
     "checkJs": true /* Report errors in .js files. */,
--
2.28.0