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
22 changes: 22 additions & 0 deletions __tests__/resolve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {expect, test} from '@jest/globals'
import {resolveIssues} from '../src/run'

test('resolveIssues collects every issue across both relations, with per-DB status', () => {
const row = {
properties: {
'🐛 Tech Issues': {relation: [{id: 'tech-1'}, {id: 'tech-2'}]},
'✅ Task List': {relation: [{id: 'task-1'}]}
}
}
const got = resolveIssues(row)
expect(got.map(i => i.pageId)).toEqual(['tech-1', 'tech-2', 'task-1'])
expect(got[0].relation.column).toBe('🐛 Tech Issues')
expect(got[0].relation.status.for_qa).toBe('For QA')
expect(got[2].relation.column).toBe('✅ Task List')
expect(got[2].relation.status.for_qa).toBe('Ready For Review')
})

test('resolveIssues handles empty / missing relations', () => {
expect(resolveIssues({properties: {}})).toEqual([])
expect(resolveIssues(null)).toEqual([])
})
98 changes: 63 additions & 35 deletions dist/index.js

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

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

27 changes: 18 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
// Workspace config. Property names match the Notion databases; update here if they change.
export const config = {
// Tech Issues (the issues database)
// Issues database (Tech Issues) — used only by the [ISSUE-N] body fallback.
issuesDb: '0cabad23-7e62-4481-bd8c-48c8a4a08a7b',
idProp: 'ID', // unique_id property (prefix ISSUE)
issueStatusProp: 'Status',

// Pull Requests database (one row per PR, related to its issue)
// Pull Requests database one row per PR, related to its issue(s).
prsDb: '390bfd2f-80ba-80f3-88b0-ce0b805397da',
prUrlProp: 'URL',
prIssueRelation: '🐛 Tech Issues',
prStatusProp: 'Status',

issueStatus: {
in_pr: 'In PR',
fixes_required: 'Fixes Required',
for_qa: 'For QA'
},
// One entry per PRs-DB relation column. The column identifies the linked issue's
// database, and carries that database's Status vocabulary. A Tech Issue only ever
// appears in the Tech column and a task only in the Task column.
relations: [
{
column: '🐛 Tech Issues',
statusProp: 'Status',
status: {in_pr: 'In PR', fixes_required: 'Fixes Required', for_qa: 'For QA'}
},
{
column: '✅ Task List',
statusProp: 'Status',
status: {in_pr: 'In PR', fixes_required: 'Fixes Required', for_qa: 'Ready For Review'}
}
],

prStatus: {
draft: 'Draft',
open: 'Open',
Expand Down
71 changes: 47 additions & 24 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {config} from './config'
import {notion} from './notion'
import {issueStatusKey, parseUniqueId, prRowStatusKey} from './transitions'

type RelationConfig = (typeof config.relations)[number]
type LinkedIssue = {pageId: string; relation: RelationConfig}

async function findPrRow(prUrl: string | undefined, token: string): Promise<any> {
if (!prUrl) return null
const res = await notion('POST', `/databases/${config.prsDb}/query`, token, {
Expand All @@ -16,9 +19,16 @@ async function findPrRow(prUrl: string | undefined, token: string): Promise<any>
return res.results?.[0] ?? null
}

function issueIdFromRow(row: any): string | null {
const rel = row?.properties?.[config.prIssueRelation]?.relation ?? []
return rel[0]?.id ?? null
// Every issue linked to a PR row, across all relation columns (a PR can fix many issues).
export function resolveIssues(row: any): LinkedIssue[] {
const issues: LinkedIssue[] = []
for (const relation of config.relations) {
const rel = row?.properties?.[relation.column]?.relation ?? []
for (const r of rel) {
if (r?.id) issues.push({pageId: r.id, relation})
}
}
return issues
}

async function resolveIssueViaBody(
Expand Down Expand Up @@ -51,16 +61,17 @@ async function setStatus(
return true
}

// True if every linked PR row OTHER than the current one is Merged/Closed.
// Returns null if the rows can't be read (caller decides).
// True if every PR row OTHER than the current one linked to this issue (via the given
// relation column) is Merged/Closed. Returns null if the rows can't be read.
async function othersAllDone(
issueId: string,
currentRowId: string | null,
relationColumn: string,
token: string
): Promise<boolean | null> {
const done = new Set<string>([config.prStatus.merged, config.prStatus.closed])
const res = await notion('POST', `/databases/${config.prsDb}/query`, token, {
filter: {property: config.prIssueRelation, relation: {contains: issueId}}
filter: {property: relationColumn, relation: {contains: issueId}}
})
if (res.object === 'error') return null
const open: unknown[] = []
Expand All @@ -70,7 +81,7 @@ async function othersAllDone(
if (!done.has(status)) open.push([row.properties?.[config.prUrlProp]?.url, status])
}
if (open.length) {
core.info(`sibling PR(s) not done: ${JSON.stringify(open)}`)
core.info(`issue ${issueId}: sibling PR(s) not done: ${JSON.stringify(open)}`)
return false
}
return true
Expand All @@ -80,9 +91,17 @@ export async function run(event: any, token: string): Promise<void> {
const pr = event.pull_request ?? {}
const prUrl: string | undefined = pr.html_url
const row = await findPrRow(prUrl, token)
const issueId = row ? issueIdFromRow(row) : await resolveIssueViaBody(pr.body, token)

// 1) PR row Status (Draft / Open / Merged / Closed)
// Every issue this PR is linked to (a PR can fix several, across both databases).
let issues: LinkedIssue[]
if (row) {
issues = resolveIssues(row)
} else {
const fallbackId = await resolveIssueViaBody(pr.body, token)
issues = fallbackId ? [{pageId: fallbackId, relation: config.relations[0]}] : []
}

// 1) PR row Status (Draft / Open / Merged / Closed) — one row.
const rowKey = prRowStatusKey(event)
if (rowKey) {
if (row) {
Expand All @@ -94,28 +113,32 @@ export async function run(event: any, token: string): Promise<void> {
}
}

// 2) Issue Status (In PR / Fixes Required / For QA)
// 2) Issue Status (In PR / Fixes Required / For QA) — for EVERY linked issue.
const issueKey = issueStatusKey(event)
if (!issueKey) {
core.info('No issue-status transition for this event.')
return
}
if (!issueId) {
core.info(`Could not resolve a Notion issue for ${prUrl}; skipping issue status.`)
if (issues.length === 0) {
core.info(`Could not resolve any Notion issue for ${prUrl}; skipping issue status.`)
return
}
if (issueKey === 'for_qa') {
const done = await othersAllDone(issueId, row?.id ?? null, token)
if (done === false) {
core.info('Merged, but sibling PRs are still open; leaving issue status unchanged.')
return
}
if (done === null) {
core.warning('Could not read sibling rows; leaving issue status unchanged (fail-safe).')
return

for (const {pageId, relation} of issues) {
// "For QA" only when all of THIS issue's own PRs are merged/closed.
if (issueKey === 'for_qa') {
const done = await othersAllDone(pageId, row?.id ?? null, relation.column, token)
if (done === false) {
core.info(`issue ${pageId}: sibling PRs still open; leaving unchanged.`)
continue
}
if (done === null) {
core.warning(`issue ${pageId}: could not read sibling rows; leaving unchanged (fail-safe).`)
continue
}
}
const name = relation.status[issueKey]
core.info(`set issue ${pageId} -> ${name} (via ${relation.column})`)
await setStatus(pageId, relation.statusProp, name, token)
}
const name = config.issueStatus[issueKey]
core.info(`set issue -> ${name}`)
await setStatus(issueId, config.issueStatusProp, name, token)
}
Loading