Skip to content

Commit 0f39adc

Browse files
committed
foces
1 parent 13e7393 commit 0f39adc

7 files changed

Lines changed: 217 additions & 41 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ It gives org admins a place to manage CLA text and signing history, and gives co
1111
- Admins can define an org-scoped bypass list of GitHub accounts that should always receive a passing CLA check.
1212
- If a contributor has open pull requests and their signature becomes outdated after a CLA update, checks may need to be re-opened/re-evaluated and set to failing until re-signing is completed.
1313
- After a contributor signs/re-signs the latest CLA, the app schedules an async workflow that updates their open PR CLA checks to success and removes stale CLA prompt comments.
14+
- When an org is activated/deactivated, the app schedules an async workflow to re-check open PRs for that org so checks converge to the new enforcement mode.
1415
- GitHub is the user-management source of truth for the app.
1516
- The app has no local signup/password user-management system; DB user rows are GitHub-linked identity mirrors only.
1617
- Authentication/session management is stateless JWT-based (HTTP-only cookie + signed JWT with `jti`).
@@ -179,7 +180,8 @@ This section amends your scenario list and adds missing scenarios.
179180

180181
### 9) Additional scenarios commonly missed
181182

182-
- Org deactivated/uninstalled: signing blocked, webhook checks/comments skipped.
183+
- Org deactivated/uninstalled: signing blocked; webhook events set passing CLA checks and remove managed CLA prompts so PRs are not blocked by CLA while inactive.
184+
- Activating or deactivating an org schedules an async open-PR recheck workflow so existing PR checks/comments converge automatically.
183185
- Updating bypass list schedules async open-PR recheck so existing PRs converge to the latest policy.
184186
- `/recheck` authorization: allowed for PR author, org member, or maintainer; unauthorized users are blocked.
185187
- OAuth and install redirects sanitize `returnTo` to prevent open redirects.
@@ -230,6 +232,7 @@ This section amends your scenario list and adds missing scenarios.
230232
- Unsigned/outdated signature: failing check + bot comment with signing URL.
231233
- When CLA text changes, contributors on older signatures are marked as requiring re-sign; open PRs may require check re-evaluation and failure until re-signing.
232234
- After signing/re-signing, an async workflow updates signer-authored open PR CLA checks to success and removes stale CLA prompt comments.
235+
- Activating/deactivating CLA enforcement schedules async open-PR rechecks; inactive mode converges CLA checks to success and clears managed CLA prompt comments.
233236
- CLA bot comment updates/deletions are restricted to CLA-managed comments tagged with an internal signature marker, preventing edits to third-party bot comments.
234237
- Repository maintainers must require `CLA Bot / Contributor License Agreement` in GitHub branch protection/rulesets for merge blocking to be enforced.
235238
- Markdown ordered lists preserve explicit authored numbering (for example `1.`, `2.`, `7.` stays `1, 2, 7`), and legal alpha markers (`a.` / `a)`) render as ordered sub-clauses with indentation.

app/admin/[orgSlug]/actions.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,38 @@ export async function toggleOrganizationActiveAction(input: unknown): Promise<Ac
117117
return { ok: false, error: "Organization not found" }
118118
}
119119

120+
const headerStore = await headers()
121+
const appBaseUrl = getBaseUrlFromHeaders(headerStore)
122+
const recheck = await scheduleClaRecheckForOrg({
123+
orgSlug,
124+
orgId: org.id,
125+
claSha256: org.claTextSha256,
126+
appBaseUrl,
127+
actor: {
128+
userId: access.user.id,
129+
githubId: access.user.githubId ?? null,
130+
githubUsername: access.user.githubUsername ?? null,
131+
},
132+
})
133+
120134
await createAuditEvent({
121135
eventType: "organization.activation_changed",
122136
orgId: org.id,
123137
userId: access.user.id,
124138
actorGithubId: access.user.githubId ?? null,
125139
actorGithubUsername: access.user.githubUsername,
126-
payload: { isActive },
140+
payload: {
141+
isActive,
142+
recheckScheduled: recheck.recheckScheduled,
143+
recheckRunId: recheck.recheckRunId,
144+
recheckScheduleError: recheck.recheckScheduleError,
145+
},
127146
})
128147

129148
revalidatePath(`/admin/${orgSlug}`)
130149
revalidatePath("/admin")
131150

132-
return { ok: true }
151+
return { ok: true, ...recheck }
133152
}
134153

135154
export async function addBypassAccountAction(input: unknown): Promise<ActionResult> {

app/api/webhook/github/route.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,13 +312,6 @@ async function handlePrCheck(params: {
312312
return NextResponse.json({ error: `Organization "${orgSlug}" not found` }, { status: 404 })
313313
}
314314

315-
if (!org.isActive) {
316-
return NextResponse.json({
317-
message: `CLA bot is deactivated for ${orgSlug}. No check or comment.`,
318-
skipped: true,
319-
})
320-
}
321-
322315
const resolvedInstallationId = installationId ?? org.installationId ?? undefined
323316
if (installationId && org.installationId !== installationId) {
324317
await updateOrganizationInstallationId(orgSlug, installationId)
@@ -339,6 +332,59 @@ async function handlePrCheck(params: {
339332
return NextResponse.json({ error: "GitHub client is not configured" }, { status: 500 })
340333
}
341334

335+
if (!org.isActive) {
336+
const check = await github.createCheckRun({
337+
owner: orgSlug,
338+
repo: repoName,
339+
name: CHECK_NAME,
340+
head_sha: headSha,
341+
status: "completed",
342+
conclusion: "success",
343+
output: {
344+
title: "CLA: Bot deactivated",
345+
summary: `CLA enforcement is currently deactivated for @${orgSlug}. This pull request is not blocked by CLA requirements.`,
346+
},
347+
})
348+
349+
const existingComment = await github.findBotComment(orgSlug, repoName, prNumber)
350+
let deletedCommentId: number | null = null
351+
if (existingComment && isRemovableClaPromptComment(existingComment.body)) {
352+
await github.deleteComment({
353+
owner: orgSlug,
354+
repo: repoName,
355+
comment_id: existingComment.id,
356+
})
357+
deletedCommentId = existingComment.id
358+
}
359+
360+
await createAuditEvent({
361+
eventType: "webhook.pr_check",
362+
orgId: org.id,
363+
actorGithubId: prAuthorId ? String(prAuthorId) : null,
364+
actorGithubUsername: prAuthor,
365+
payload: {
366+
owner: orgSlug,
367+
repo: repoName,
368+
prNumber,
369+
decision: "inactive",
370+
checkConclusion: check.conclusion,
371+
deletedCommentId,
372+
},
373+
})
374+
375+
return NextResponse.json({
376+
message: `CLA bot is deactivated for ${orgSlug}. Check set to success and CLA prompts removed.`,
377+
skipped: true,
378+
check: { id: check.id, status: "success", conclusion: check.conclusion },
379+
comment: null,
380+
orgMember: false,
381+
accountOwner: false,
382+
signed: true,
383+
needsResign: false,
384+
inactive: true,
385+
})
386+
}
387+
342388
const bypassAccount = await isBypassAccountForOrg({
343389
orgId: org.id,
344390
githubUserId: prAuthorId,

components/admin/org-manage-client.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,16 +227,25 @@ export function OrgManageClient({
227227
function handleToggleActive() {
228228
startToggleActiveTransition(async () => {
229229
setError(null)
230+
const nextIsActive = !org.isActive
230231
const result = await toggleOrganizationActiveAction({
231232
orgSlug: org.githubOrgSlug,
232-
isActive: !org.isActive,
233+
isActive: nextIsActive,
233234
})
234235

235236
if (!result.ok) {
236237
setError(result.error ?? "Failed to update activation status")
237238
return
238239
}
239240

241+
if (result.recheckScheduleError) {
242+
setError(
243+
nextIsActive
244+
? "Organization activated, but async PR recheck scheduling failed."
245+
: "Organization deactivated, but async PR recheck scheduling failed."
246+
)
247+
}
248+
240249
router.refresh()
241250
})
242251
}

components/markdown-renderer.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,27 @@ export function simpleMarkdownToHtml(md: string): string {
4545
// Process lists and blockquotes
4646
const lines = html.split("\n")
4747
const result: string[] = []
48-
let listState: {
49-
kind: "ol" | "ul" | "ol-alpha-lower" | "ol-alpha-upper"
50-
indent: number
51-
} | null = null
48+
type ListKind = "ol" | "ul" | "ol-alpha-lower" | "ol-alpha-upper"
49+
type ListState = { kind: ListKind; indent: number }
50+
let listState: ListState | null = null
5251
let inBlockquote = false
5352

54-
const closeList = () => {
55-
if (!listState) return
56-
result.push(listState.kind === "ul" ? "</ul>" : "</ol>")
57-
listState = null
53+
const closeList = (state: ListState | null) => {
54+
if (!state) return null
55+
result.push(state.kind === "ul" ? "</ul>" : "</ol>")
56+
return null
5857
}
5958

60-
const openList = (kind: "ol" | "ul" | "ol-alpha-lower" | "ol-alpha-upper", indent: number) => {
59+
const openList = (kind: ListKind, indent: number): ListState => {
6160
if (kind === "ul") {
6261
result.push(`<ul${listIndentStyle(indent)}>`)
63-
listState = { kind, indent }
64-
return
62+
return { kind, indent }
6563
}
6664

6765
const typeAttr =
6866
kind === "ol-alpha-lower" ? ' type="a"' : kind === "ol-alpha-upper" ? ' type="A"' : ""
6967
result.push(`<ol${typeAttr}${listIndentStyle(indent)}>`)
70-
listState = { kind, indent }
68+
return { kind, indent }
7169
}
7270

7371
for (let i = 0; i < lines.length; i++) {
@@ -84,8 +82,8 @@ export function simpleMarkdownToHtml(md: string): string {
8482
inBlockquote = false
8583
}
8684
if (!listState || listState.kind !== "ol" || listState.indent !== indent) {
87-
closeList()
88-
openList("ol", indent)
85+
listState = closeList(listState)
86+
listState = openList("ol", indent)
8987
}
9088
result.push(`<li value="${olMatch[2]}">${olMatch[3]}</li>`)
9189
} else if (alphaMatch) {
@@ -99,8 +97,8 @@ export function simpleMarkdownToHtml(md: string): string {
9997
inBlockquote = false
10098
}
10199
if (!listState || listState.kind !== listKind || listState.indent !== indent) {
102-
closeList()
103-
openList(listKind, indent)
100+
listState = closeList(listState)
101+
listState = openList(listKind, indent)
104102
}
105103
result.push(`<li value="${markerValue}">${alphaMatch[3]}</li>`)
106104
} else if (ulMatch) {
@@ -110,12 +108,12 @@ export function simpleMarkdownToHtml(md: string): string {
110108
inBlockquote = false
111109
}
112110
if (!listState || listState.kind !== "ul" || listState.indent !== indent) {
113-
closeList()
114-
openList("ul", indent)
111+
listState = closeList(listState)
112+
listState = openList("ul", indent)
115113
}
116114
result.push(`<li>${ulMatch[2]}</li>`)
117115
} else if (bqMatch) {
118-
closeList()
116+
listState = closeList(listState)
119117
if (!inBlockquote) {
120118
result.push("<blockquote>")
121119
inBlockquote = true
@@ -124,7 +122,7 @@ export function simpleMarkdownToHtml(md: string): string {
124122
result.push(`<p>${bqMatch[1]}</p>`)
125123
}
126124
} else {
127-
closeList()
125+
listState = closeList(listState)
128126
if (inBlockquote) {
129127
result.push("</blockquote>")
130128
inBlockquote = false
@@ -145,7 +143,7 @@ export function simpleMarkdownToHtml(md: string): string {
145143
}
146144
}
147145

148-
closeList()
146+
listState = closeList(listState)
149147
if (inBlockquote) result.push("</blockquote>")
150148

151149
return result.join("\n")

lib/cla/recheck-open-prs.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type ClaOpenPrRecheckSummary = {
1414
rechecked: number
1515
failedChecks: number
1616
passedBypassChecks: number
17+
passedInactiveChecks: number
1718
commentsCreated: number
1819
commentsUpdated: number
1920
commentsDeleted: number
@@ -35,6 +36,7 @@ export async function recheckOpenPullRequestsAfterClaUpdate(params: {
3536
rechecked: 0,
3637
failedChecks: 0,
3738
passedBypassChecks: 0,
39+
passedInactiveChecks: 0,
3840
commentsCreated: 0,
3941
commentsUpdated: 0,
4042
commentsDeleted: 0,
@@ -51,11 +53,7 @@ export async function recheckOpenPullRequestsAfterClaUpdate(params: {
5153
summary.error = `Organization "${params.orgSlug}" not found`
5254
return summary
5355
}
54-
55-
if (!org.isActive) {
56-
summary.skippedInactive = true
57-
return summary
58-
}
56+
summary.skippedInactive = !org.isActive
5957

6058
const resolvedInstallationId = params.installationId ?? org.installationId ?? undefined
6159
let github: ReturnType<typeof getGitHubClient>
@@ -81,6 +79,35 @@ export async function recheckOpenPullRequestsAfterClaUpdate(params: {
8179

8280
for (const pr of openPrs) {
8381
try {
82+
if (!org.isActive) {
83+
await github.createCheckRun({
84+
owner: params.orgSlug,
85+
repo: pr.repoName,
86+
name: CHECK_NAME,
87+
head_sha: pr.headSha,
88+
status: "completed",
89+
conclusion: "success",
90+
output: {
91+
title: "CLA: Bot deactivated",
92+
summary: `CLA enforcement is currently deactivated for @${params.orgSlug}. This pull request is not blocked by CLA requirements.`,
93+
},
94+
})
95+
96+
const existingComment = await github.findBotComment(params.orgSlug, pr.repoName, pr.number)
97+
if (existingComment && isRemovableClaPromptComment(existingComment.body)) {
98+
await github.deleteComment({
99+
owner: params.orgSlug,
100+
repo: pr.repoName,
101+
comment_id: existingComment.id,
102+
})
103+
summary.commentsDeleted += 1
104+
}
105+
106+
summary.passedInactiveChecks += 1
107+
summary.rechecked += 1
108+
continue
109+
}
110+
84111
const bypassAccount = await isBypassAccountForOrg({
85112
orgId: org.id,
86113
githubUserId: pr.authorId,

0 commit comments

Comments
 (0)