summaryrefslogtreecommitdiffhomepage
path: root/.github/workflows/close-stale-prs.yml
blob: e1ff4241c98813727614905e5f6e74868df0fb16 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
name: close-stale-prs

on:
  workflow_dispatch:
    inputs:
      dryRun:
        description: "Log actions without closing PRs"
        type: boolean
        default: false
  schedule:
    - cron: "0 6 * * *"

permissions:
  contents: read
  issues: write
  pull-requests: write

jobs:
  close-stale-prs:
    runs-on: ubuntu-latest
    steps:
      - name: Close inactive PRs
        uses: actions/github-script@v8
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const DAYS_INACTIVE = 60
            const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
            const { owner, repo } = context.repo
            const dryRun = context.payload.inputs?.dryRun === "true"

            core.info(`Dry run mode: ${dryRun}`)
            core.info(`Cutoff date: ${cutoff.toISOString()}`)

            const query = `
              query($owner: String!, $repo: String!, $cursor: String) {
                repository(owner: $owner, name: $repo) {
                  pullRequests(first: 100, states: OPEN, after: $cursor) {
                    pageInfo {
                      hasNextPage
                      endCursor
                    }
                    nodes {
                      number
                      title
                      author {
                        login
                      }
                      createdAt
                      commits(last: 1) {
                        nodes {
                          commit {
                            committedDate
                          }
                        }
                      }
                      comments(last: 1) {
                        nodes {
                          createdAt
                        }
                      }
                      reviews(last: 1) {
                        nodes {
                          createdAt
                        }
                      }
                    }
                  }
                }
              }
            `

            const allPrs = []
            let cursor = null
            let hasNextPage = true

            while (hasNextPage) {
              const result = await github.graphql(query, {
                owner,
                repo,
                cursor,
              })

              allPrs.push(...result.repository.pullRequests.nodes)
              hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
              cursor = result.repository.pullRequests.pageInfo.endCursor
            }

            core.info(`Found ${allPrs.length} open pull requests`)

            const stalePrs = allPrs.filter((pr) => {
              const dates = [
                new Date(pr.createdAt),
                pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null,
                pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null,
                pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null,
              ].filter((d) => d !== null)

              const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0]

              if (!lastActivity || lastActivity > cutoff) {
                core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`)
                return false
              }

              core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`)
              return true
            })

            if (!stalePrs.length) {
              core.info("No stale pull requests found.")
              return
            }

            core.info(`Found ${stalePrs.length} stale pull requests`)

            for (const pr of stalePrs) {
              const issue_number = pr.number
              const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`

              if (dryRun) {
                core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
                continue
              }

              await github.rest.issues.createComment({
                owner,
                repo,
                issue_number,
                body: closeComment,
              })

              await github.rest.pulls.update({
                owner,
                repo,
                pull_number: issue_number,
                state: "closed",
              })

              core.info(`Closed PR #${issue_number} from ${pr.author.login}: ${pr.title}`)
            }