Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"ci": "bun run format && bun run lint && bun run check:types && bun run test",
"build": "bun build --compile --outfile bin/no-response-bin ./src/main.ts",
"check:types": "bun x --package=typescript -- tsc --noEmit --moduleResolution bundler --module es2022 --target es2022 --lib es2022,dom src/main.ts",
"check:types": "bun x --package=typescript -- tsc --noEmit --moduleResolution bundler --module es2022 --target es2022 --lib es2022,dom --types node src/main.ts",
"format:write": "bun x -- prettier --write **/*.ts **/*.yaml **/*.yml",
"format": "bun x -- prettier --check **/*.ts **/*.yaml **/*.yml",
"lint": "bun x -- eslint src/**/*.ts",
Expand Down Expand Up @@ -36,6 +36,6 @@
"eslint": "^10.0.0",
"eslint-plugin-github": "^6.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
"typescript": "^6.0.0"
}
}
22 changes: 18 additions & 4 deletions src/gh-api-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,32 @@ export function toDate(dateStr?: string | null): Date | undefined {
* Maps a raw GitHub API response to a clean IssueDetails object.
*/
export function mapRestIssue(raw: any, repo: Repository): IssueDetails {
return {
const closedAt = toDate(raw.closed_at)
const date_closedAt = closedAt ?? new Date()

const openIssue = {
number: raw.number,
repo,
state: raw.state,
state: 'open' as 'open',
user: { login: raw.user?.login || 'unknown' },
labels: (raw.labels || []).map((l: any) => ({
name: 'string' === typeof l ? l : l.name!,
repo,
color: ('string' === typeof l ? '' : l.color) || 'ffffff'
})),
closed_by: raw.closed_by ? { login: raw.closed_by.login } : undefined,
closed_at: toDate(raw.closed_at)
closed_by: undefined,
closed_at: undefined
}

if ('closed' === raw.state) {
return {
...openIssue,
state: 'closed' as 'closed',
closed_by: raw.closed_by ? { login: raw.closed_by.login } : undefined,
closed_at: date_closedAt
}
} else {
return openIssue
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/issue-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { GitHubApiClient } from './gh-api-client'
import { findLastClosedEvent } from './logic-helpers'
import { RepoMetadataCache } from './repo-metadata-cache'
import { Issue, IssueDetails, Label, Repository, TimelineEvent } from './types'
import { ClosedIssueDetails, Issue, IssueDetails, Label, Repository, TimelineEvent } from './types'

export class IssueCache {
// Key format: "repoNodeId:issueNumber"
Expand Down Expand Up @@ -101,19 +101,20 @@ export class IssueCache {
return { details, timeline: undefined }
}

const closedIssue: ClosedIssueDetails = details
const timeline = await this.client.fetchTimeline(details)
const lastClosed = findLastClosedEvent(timeline)
if (lastClosed === undefined) {
if (missingClosedAt) details.closed_at = new Date(0)
if (missingCloser) details.closed_by = { login: 'unknown' }
if (missingClosedAt) closedIssue.closed_at = new Date(0)
if (missingCloser) closedIssue.closed_by = { login: 'unknown' }
return { details, timeline }
}

// Patch what we can from timeline
if (missingCloser || suspiciousCloser) details.closed_by = { login: lastClosed.actor.login }
if (missingCloser || suspiciousCloser) closedIssue.closed_by = { login: lastClosed.actor.login }

// Optional: timeline "closed" created_at can be used if REST closed_at is missing
if (missingClosedAt) details.closed_at = lastClosed.created_at
if (missingClosedAt) closedIssue.closed_at = lastClosed.created_at

await this.set(details)
return { details, timeline }
Expand Down
14 changes: 9 additions & 5 deletions src/no-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
isTargetLabeledEvent
} from './logic-helpers'
import { RepoMetadataCache } from './repo-metadata-cache'
import { Repository, Label, Issue, IssueDetails } from './types'
import { ClosedIssueDetails, Repository, Label, Issue, IssueDetails } from './types'

export default class NoResponse {
private gracePeriodMs = 1000 * 60 * 15 // minutes
Expand Down Expand Up @@ -179,7 +179,7 @@ export default class NoResponse {
* CASE 1: Closed by someone else (Bot/Maintainer).
* Reopen immediately on author response.
*/
if (!isAuthorClosed && 'closed' === issueDetails.state) {
if (!isAuthorClosed && 'closed' === details.state) {
core.info(
`Author responded to closed issue ${this.repository.owner}/${this.repository.name}#${issueDetails.number}. Reopening.`
)
Expand All @@ -191,8 +191,9 @@ export default class NoResponse {
* CASE 2: Closed by the author themselves.
* Reopen only if the comment happened after the grace period.
*/
if (isAuthorClosed) {
const closedAt = details.closed_at.getTime()
if (isAuthorClosed && 'closed' === details.state) {
const closedIssue: ClosedIssueDetails = details
const closedAt = closedIssue.closed_at.getTime()
const createdAt = toDate(payload.comment.created_at)
const commentedAt = createdAt ? createdAt.getTime() : Date.now()

Expand Down Expand Up @@ -262,10 +263,13 @@ export default class NoResponse {
const reopenable: IssueDetails[] = []
for (const raw of results) {
const issueDetails = await this.issueCache.fetch(this.repository, raw.number)
if ('closed' !== issueDetails.state) continue
const { details, timeline } = await this.issueCache.ensureClosureDetails(issueDetails)
if ('closed' !== details.state) continue

const closedIssue: ClosedIssueDetails = details
const closedAt =
(checkClosedByAuthor(details) ? this.gracePeriodMs : 0) + details.closed_at.getTime()
(checkClosedByAuthor(details) ? this.gracePeriodMs : 0) + closedIssue.closed_at.getTime()
const events = timeline ?? (await this.client.fetchTimeline(details))
const authorResponded = events.some(
(e) =>
Expand Down