Skip to content

Commit dbbc1e2

Browse files
authored
Add AUTOTITLE-like functionality for audit log reference links (#56232)
1 parent 011df84 commit dbbc1e2

File tree

7 files changed

+220
-59
lines changed

7 files changed

+220
-59
lines changed

data/ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ parameter_table:
144144
audit_logs:
145145
action: Action
146146
description: Description
147+
reference: Reference
147148
graphql:
148149
reference:
149150
implements: <code>{{ GraphQLItemTitle }}</code> Implements

src/audit-logs/components/GroupedEvents.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,40 @@ export default function GroupedEvents({ auditLogEvents, category }: Props) {
2222
<tr>
2323
<th scope="col">{t('action')}</th>
2424
<th scope="col">{t('description')}</th>
25+
<th scope="col">{t('reference')}</th>
2526
</tr>
2627
</thead>
2728
<tbody>
2829
{auditLogEvents.map((event) => {
30+
const renderReferenceLinks = () => {
31+
if (!event.docs_reference_links || event.docs_reference_links === 'N/A') {
32+
return null
33+
}
34+
35+
const links = event.docs_reference_links
36+
.split(/[,\s]+/)
37+
.map((link) => link.trim())
38+
.filter((link) => link && link !== 'N/A')
39+
40+
const titles = event.docs_reference_titles
41+
? event.docs_reference_titles.split(', ')
42+
: links
43+
44+
return links.map((link, index) => (
45+
<span key={link}>
46+
<a href={link}>{titles[index] || link}</a>
47+
{index < links.length - 1 && ', '}
48+
</span>
49+
))
50+
}
51+
2952
return (
3053
<tr key={event.action}>
3154
<td>
3255
<code>{event.action}</code>
3356
</td>
3457
<td>{event.description}</td>
58+
<td>{renderReferenceLinks()}</td>
3559
</tr>
3660
)
3761
})}

src/audit-logs/lib/index.ts

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'path'
22

33
import { readCompressedJsonFileFallback } from '@/frame/lib/read-json-file'
44
import { getOpenApiVersion } from '@/versions/lib/all-versions'
5+
import findPage from '@/frame/lib/find-page'
56
import type {
67
AuditLogEventT,
78
CategorizedEvents,
@@ -20,6 +21,61 @@ type PipelineConfig = {
2021
appendedDescriptions: Record<string, string>
2122
}
2223

24+
type TitleResolutionContext = {
25+
pages: Record<string, any>
26+
redirects: Record<string, string>
27+
}
28+
29+
// Resolves docs_reference_links URLs to page titles
30+
async function resolveReferenceLinksToTitles(
31+
docsReferenceLinks: string,
32+
context: TitleResolutionContext,
33+
): Promise<string> {
34+
if (!docsReferenceLinks || docsReferenceLinks === 'N/A') {
35+
return ''
36+
}
37+
38+
// Handle multiple comma-separated or space-separated links
39+
const links = docsReferenceLinks
40+
.split(/[,\s]+/)
41+
.map((link) => link.trim())
42+
.filter((link) => link && link !== 'N/A')
43+
44+
const titles = []
45+
for (const link of links) {
46+
try {
47+
const page = findPage(link, context.pages, context.redirects)
48+
if (page) {
49+
// Create a minimal context for rendering the title
50+
const renderContext = {
51+
currentLanguage: 'en',
52+
currentVersion: 'free-pro-team@latest',
53+
pages: context.pages,
54+
redirects: context.redirects,
55+
}
56+
const title = await page.renderProp('title', renderContext, { textOnly: true })
57+
titles.push(title)
58+
} else {
59+
// If we can't resolve the link, use the original URL
60+
titles.push(link)
61+
}
62+
} catch (error) {
63+
// If resolution fails, use the original URL
64+
console.warn(
65+
`Failed to resolve title for link: ${link}`,
66+
error instanceof Error
67+
? error instanceof Error
68+
? error.message
69+
: String(error)
70+
: String(error),
71+
)
72+
titles.push(link)
73+
}
74+
}
75+
76+
return titles.join(', ')
77+
}
78+
2379
// get audit log event data for the requested page and version
2480
//
2581
// returns an array of event objects that look like this:
@@ -87,17 +143,19 @@ export function getCategorizedAuditLogEvents(page: string, version: string) {
87143
}
88144

89145
// Filters audit log events based on allowlist values.
90-
//
91-
// * eventsToCheck: events to consider
92-
// * allowListvalues: allowlist values to filter by
93-
// * currentEvents: events already collected
94-
// * pipelineConfig: audit log pipeline config data
95-
export function filterByAllowlistValues(
96-
eventsToCheck: RawAuditLogEventT[],
97-
allowListValues: string | string[],
98-
currentEvents: AuditLogEventT[],
99-
pipelineConfig: PipelineConfig,
100-
) {
146+
export async function filterByAllowlistValues({
147+
eventsToCheck,
148+
allowListValues,
149+
currentEvents = [],
150+
pipelineConfig,
151+
titleContext,
152+
}: {
153+
eventsToCheck: RawAuditLogEventT[]
154+
allowListValues: string | string[]
155+
currentEvents?: AuditLogEventT[]
156+
pipelineConfig: PipelineConfig
157+
titleContext?: TitleResolutionContext
158+
}) {
101159
if (!Array.isArray(allowListValues)) allowListValues = [allowListValues]
102160
if (!currentEvents) currentEvents = []
103161

@@ -112,12 +170,27 @@ export function filterByAllowlistValues(
112170
if (seen.has(event.action)) continue
113171
seen.add(event.action)
114172

115-
const minimal = {
173+
const minimal: AuditLogEventT = {
116174
action: event.action,
117175
description: processAndGetEventDescription(event, eventAllowlists, pipelineConfig),
118176
docs_reference_links: event.docs_reference_links,
119177
}
120178

179+
// Resolve reference link titles if context is provided
180+
if (titleContext && event.docs_reference_links && event.docs_reference_links !== 'N/A') {
181+
try {
182+
minimal.docs_reference_titles = await resolveReferenceLinksToTitles(
183+
event.docs_reference_links,
184+
titleContext,
185+
)
186+
} catch (error) {
187+
console.warn(
188+
`Failed to resolve titles for event ${event.action}:`,
189+
error instanceof Error ? error.message : String(error),
190+
)
191+
}
192+
}
193+
121194
minimalEvents.push(minimal)
122195
}
123196
}
@@ -132,6 +205,7 @@ export function filterByAllowlistValues(
132205
// * currentEvents: events already collected
133206
// * pipelineConfig: audit log pipeline config data
134207
// * auditLogPage: the audit log page the event belongs to
208+
// * titleContext: optional context for resolving reference link titles
135209
//
136210
// Mutates `currentGhesEvents` and updates it with any new filtered for audit
137211
// log events, the object maps GHES versions to page events for that version e.g.:
@@ -148,13 +222,21 @@ export function filterByAllowlistValues(
148222
// user: [...],
149223
// },
150224
// }
151-
export function filterAndUpdateGhesDataByAllowlistValues(
152-
eventsToCheck: RawAuditLogEventT[],
153-
allowListValue: string,
154-
currentGhesEvents: VersionedAuditLogData,
155-
pipelineConfig: PipelineConfig,
156-
auditLogPage: string,
157-
) {
225+
export async function filterAndUpdateGhesDataByAllowlistValues({
226+
eventsToCheck,
227+
allowListValue,
228+
currentGhesEvents,
229+
pipelineConfig,
230+
auditLogPage,
231+
titleContext,
232+
}: {
233+
eventsToCheck: RawAuditLogEventT[]
234+
allowListValue: string
235+
currentGhesEvents: VersionedAuditLogData
236+
pipelineConfig: PipelineConfig
237+
auditLogPage: string
238+
titleContext?: TitleResolutionContext
239+
}) {
158240
if (!currentGhesEvents) currentGhesEvents = {}
159241

160242
const seenByGhesVersion = new Map()
@@ -173,12 +255,27 @@ export function filterAndUpdateGhesDataByAllowlistValues(
173255
if (seenByGhesVersion.get(fullGhesVersion)?.has(event.action)) continue
174256

175257
if (ghesVersionAllowlists.includes(allowListValue)) {
176-
const minimal = {
258+
const minimal: AuditLogEventT = {
177259
action: event.action,
178260
description: processAndGetEventDescription(event, ghesVersionAllowlists, pipelineConfig),
179261
docs_reference_links: event.docs_reference_links,
180262
}
181263

264+
// Resolve reference link titles if context is provided
265+
if (titleContext && event.docs_reference_links && event.docs_reference_links !== 'N/A') {
266+
try {
267+
minimal.docs_reference_titles = await resolveReferenceLinksToTitles(
268+
event.docs_reference_links,
269+
titleContext,
270+
)
271+
} catch (error) {
272+
console.warn(
273+
`Failed to resolve titles for event ${event.action}:`,
274+
error instanceof Error ? error.message : String(error),
275+
)
276+
}
277+
}
278+
182279
// we need to initialize as we go to build up the `minimalEvents`
183280
// object that we'll return which will contain the GHES events for
184281
// each versions + page type combos e.g. when processing GHES events

src/audit-logs/scripts/sync.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import path from 'path'
1515
import { filterByAllowlistValues, filterAndUpdateGhesDataByAllowlistValues } from '../lib/index'
1616
import { getContents, getCommitSha } from '@/workflows/git-utils'
1717
import { latest, latestStable, releaseCandidate } from '@/versions/lib/enterprise-server-releases'
18+
import { loadPages, loadPageMap } from '@/frame/lib/page-data'
19+
import loadRedirects from '@/redirects/lib/precompile'
1820
import type { AuditLogEventT, VersionedAuditLogData } from '../types'
1921

2022
if (!process.env.GITHUB_TOKEN) {
@@ -53,6 +55,13 @@ async function main() {
5355
pipelineConfig.sha = mainSha
5456
await writeFile(configFilepath, JSON.stringify(pipelineConfig, null, 2))
5557

58+
// Load pages and redirects for title resolution
59+
console.log('Loading pages and redirects for title resolution...')
60+
const pageList = await loadPages(undefined, ['en'])
61+
const pages = await loadPageMap(pageList)
62+
const redirects = await loadRedirects(pageList)
63+
const titleContext = { pages, redirects }
64+
5665
// store an array of audit log event data keyed by version and audit log page,
5766
// will look like this (depends on supported GHES versions):
5867
//
@@ -75,32 +84,39 @@ async function main() {
7584
// Wrapper around filterByAllowlistValues() because we always need all the
7685
// schema events and pipeline config data.
7786
const filter = (allowListValues: string | string[], currentEvents: AuditLogEventT[] = []) =>
78-
filterByAllowlistValues(schemaEvents, allowListValues, currentEvents, pipelineConfig)
87+
filterByAllowlistValues({
88+
eventsToCheck: schemaEvents,
89+
allowListValues,
90+
currentEvents,
91+
pipelineConfig,
92+
titleContext,
93+
})
7994
// Wrapper around filterGhesByAllowlistValues() because we always need all the
8095
// schema events and pipeline config data.
8196
const filterAndUpdateGhes = (
82-
allowListValues: string,
97+
allowListValue: string,
8398
auditLogPage: string,
84-
currentEvents: VersionedAuditLogData,
99+
currentGhesEvents: VersionedAuditLogData,
85100
) =>
86-
filterAndUpdateGhesDataByAllowlistValues(
87-
schemaEvents,
88-
allowListValues,
89-
currentEvents,
101+
filterAndUpdateGhesDataByAllowlistValues({
102+
eventsToCheck: schemaEvents,
103+
allowListValue,
104+
currentGhesEvents,
90105
pipelineConfig,
91106
auditLogPage,
92-
)
107+
titleContext,
108+
})
93109

94110
auditLogData.fpt = {}
95-
auditLogData.fpt.user = filter('user')
96-
auditLogData.fpt.organization = filter(['organization', 'org_api_only'])
111+
auditLogData.fpt.user = await filter('user')
112+
auditLogData.fpt.organization = await filter(['organization', 'org_api_only'])
97113

98114
auditLogData.ghec = {}
99-
auditLogData.ghec.user = filter('user')
100-
auditLogData.ghec.organization = filter('organization')
101-
auditLogData.ghec.organization = filter('org_api_only', auditLogData.ghec.organization)
102-
auditLogData.ghec.enterprise = filter('business')
103-
auditLogData.ghec.enterprise = filter('business_api_only', auditLogData.ghec.enterprise)
115+
auditLogData.ghec.user = await filter('user')
116+
auditLogData.ghec.organization = await filter('organization')
117+
auditLogData.ghec.organization = await filter('org_api_only', auditLogData.ghec.organization)
118+
auditLogData.ghec.enterprise = await filter('business')
119+
auditLogData.ghec.enterprise = await filter('business_api_only', auditLogData.ghec.enterprise)
104120

105121
// GHES versions are numbered (i.e. "3.9", "3.10", etc.) and filterGhes()
106122
// gives us back an object of GHES versions to page events for each version
@@ -114,11 +130,15 @@ async function main() {
114130
// so there's no single auditLogData.ghes like the other versions.
115131
const ghesVersionsAuditLogData = {}
116132

117-
filterAndUpdateGhes('business', AUDIT_LOG_PAGES.ENTERPRISE, ghesVersionsAuditLogData)
118-
filterAndUpdateGhes('business_api_only', AUDIT_LOG_PAGES.ENTERPRISE, ghesVersionsAuditLogData)
119-
filterAndUpdateGhes('user', AUDIT_LOG_PAGES.USER, ghesVersionsAuditLogData)
120-
filterAndUpdateGhes('organization', AUDIT_LOG_PAGES.ORGANIZATION, ghesVersionsAuditLogData)
121-
filterAndUpdateGhes('org_api_only', AUDIT_LOG_PAGES.ORGANIZATION, ghesVersionsAuditLogData)
133+
await filterAndUpdateGhes('business', AUDIT_LOG_PAGES.ENTERPRISE, ghesVersionsAuditLogData)
134+
await filterAndUpdateGhes(
135+
'business_api_only',
136+
AUDIT_LOG_PAGES.ENTERPRISE,
137+
ghesVersionsAuditLogData,
138+
)
139+
await filterAndUpdateGhes('user', AUDIT_LOG_PAGES.USER, ghesVersionsAuditLogData)
140+
await filterAndUpdateGhes('organization', AUDIT_LOG_PAGES.ORGANIZATION, ghesVersionsAuditLogData)
141+
await filterAndUpdateGhes('org_api_only', AUDIT_LOG_PAGES.ORGANIZATION, ghesVersionsAuditLogData)
122142
Object.assign(auditLogData, ghesVersionsAuditLogData)
123143

124144
// We don't maintain the order of events as we process them so after filtering

0 commit comments

Comments
 (0)