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
16 changes: 11 additions & 5 deletions src/dcc-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ async function submit() {
return
}

const checks = await githubOps.getChecks(pr?.number)
const isRequired = (c: Check) => pr.requiredChecks.includes(c.name)
const [checks, required] = await Promise.all([
githubOps.getChecks(pr.number),
gitOps.mainBranch().then(b => githubOps.getAllRequiredChecks(b, pr)),
])
const isRequired = (c: Check) => required.has(c.name)
const printChecksWithRequired = (list: Check[]) => {
for (const c of list) {
printCheck(c, isRequired(c))
Expand Down Expand Up @@ -261,7 +264,10 @@ async function status() {
if (!pr) {
print('No PR was created for this branch')
} else {
const checks: Check[] = await githubOps.getChecks(pr?.number)
const [checks, required] = await Promise.all([
githubOps.getChecks(pr.number),
gitOps.mainBranch().then(b => githubOps.getAllRequiredChecks(b, pr)),
])
print(`PR #${pr.number}: ${pr.title}`)
print(pr.url)
if (pr.lastCommit) {
Expand All @@ -280,9 +286,9 @@ async function status() {
const orderOfCheck = (c: Check) =>
c.tag === 'PASSING' ? 0 : c.tag === 'PENDING' ? 1 : c.tag === 'FAILING' ? 2 : shouldNeverHappen(c)
for (const c of checks.sort((a, b) => orderOfCheck(a) - orderOfCheck(b))) {
printCheck(c, pr.requiredChecks.includes(c.name))
printCheck(c, required.has(c.name))
}
const numRequired = checks.filter(c => pr.requiredChecks.includes(c.name)).length
const numRequired = checks.filter(c => required.has(c.name)).length
print(` (${numRequired}/${checks.length} are required 🔒)`)
print()
}
Expand Down
42 changes: 42 additions & 0 deletions src/github-ops.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { GitOps } from './git-ops.js'
import { Octokit } from '@octokit/rest'
import { z } from 'zod'
import { logger } from './logger.js'

const BranchRulesSchema = z.array(
z
.object({
type: z.string(),
parameters: z
.object({
required_status_checks: z.array(z.object({ context: z.string() }).passthrough()).optional(),
})
.passthrough()
.optional(),
})
.passthrough(),
)

export type Check =
| {
tag: 'FAILING'
Expand Down Expand Up @@ -44,6 +59,33 @@ export class GithubOps {
return [...pending, ...passing, ...failing]
}

async getAllRequiredChecks(branch: string, pr: { protectionRequiredChecks: string[] }): Promise<Set<string>> {
const fromRulesets = await this.getRulesetRequiredChecks(branch)
return new Set([...pr.protectionRequiredChecks, ...fromRulesets])
}

async getRulesetRequiredChecks(branch: string): Promise<string[]> {
const r = await this.gitOps.getRepo()
try {
const resp = await this.kit.request('GET /repos/{owner}/{repo}/rules/branches/{branch}', {
owner: r.owner,
repo: r.name,
branch,
})
const rules = BranchRulesSchema.parse(resp.data)
return rules
.filter(rule => rule.type === 'required_status_checks')
.flatMap(rule => rule.parameters?.required_status_checks ?? [])
.map(c => c.context)
} catch (err) {
const status = (err as { status?: number })?.status
if (status !== 404) {
logger.warn(`Failed to fetch ruleset required checks (status=${status ?? 'unknown'}): ${err}`)
}
return []
}
}

async merge(prNumber: number): Promise<void> {
const r = await this.gitOps.getRepo()
await this.kit.pulls.merge({ owner: r.owner, repo: r.name, pull_number: prNumber, merge_method: 'squash' })
Expand Down
6 changes: 3 additions & 3 deletions src/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface CurrentPrInfo {
mergeabilityStatus: MergeabilityStatus
url: string
openUrl: string
requiredChecks: string[]
protectionRequiredChecks: string[]
lastCommit?: {
message: string
abbreviatedOid?: string
Expand Down Expand Up @@ -201,7 +201,7 @@ export class GraphqlOps {

const mainBranch = await this.gitOps.mainBranch()
const protectionRules = repository?.branchProtectionRules?.nodes ?? []
const requiredChecks = protectionRules
const protectionRequiredChecks = protectionRules
.filter(rule => rule.requiresStatusChecks && rule.matchingRefs.nodes.some(ref => ref.name === mainBranch))
.flatMap(rule => rule.requiredStatusCheckContexts)

Expand All @@ -222,7 +222,7 @@ export class GraphqlOps {
mergeabilityStatus,
url,
openUrl,
requiredChecks,
protectionRequiredChecks,
lastCommit: commit && {
message: commit?.message,
abbreviatedOid: commit?.abbreviatedOid,
Expand Down