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
2 changes: 2 additions & 0 deletions config/kubernetes/production/deployments/webapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
annotations:
moda.github.net/allow-missing-ready-pods: '1'
spec:
replicas: 6
selector:
Expand Down
1 change: 1 addition & 0 deletions content/copilot/reference/ai-models/model-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Use this table to find a suitable model quickly, see more detail in the sections
| Model | Task area | Excels at (primary use case) | Additional capabilities | Further reading |
|-------------------------------------------------------|--------------------------------------------------|-------------------------------------------------------------------------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| {% data variables.copilot.copilot_gpt_41 %} | General-purpose coding and writing | Fast, accurate code completions and explanations | Agent mode, vision | [{% data variables.copilot.copilot_gpt_41 %} model card](https://openai.com/index/gpt-4-1/) |
| {% data variables.copilot.copilot_gpt_52 %} | Deep reasoning and debugging | Multi-step problem solving and architecture-level code analysis | Agent mode | Not available |
| {% data variables.copilot.copilot_gpt_51 %} | Deep reasoning and debugging | Multi-step problem solving and architecture-level code analysis | Agent mode | Not available |
| {% data variables.copilot.copilot_gpt_5_codex %} | General-purpose coding and writing | Fast, accurate code completions and explanations | Agent mode | [{% data variables.copilot.copilot_gpt_5_codex %} model card](https://cdn.openai.com/pdf/97cc5669-7a25-4e63-b15f-5fd5bdc4d149/gpt-5-codex-system-card.pdf) |
| {% data variables.copilot.copilot_gpt_5_mini %} | General-purpose coding and writing | Fast, accurate code completions and explanations | Agent mode, reasoning, vision | [{% data variables.copilot.copilot_gpt_5_mini %} model card](https://cdn.openai.com/gpt-5-system-card.pdf) |
Expand Down
1 change: 1 addition & 0 deletions content/copilot/reference/ai-models/model-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Used for:
* {% data variables.copilot.copilot_gpt_51_codex %}
* {% data variables.copilot.copilot_gpt_51_codex_mini %}
* {% data variables.copilot.copilot_gpt_51_codex_max %}
* {% data variables.copilot.copilot_gpt_52 %}

These models are hosted by OpenAI and {% data variables.product.github %}'s Azure infrastructure.

Expand Down
4 changes: 4 additions & 0 deletions data/tables/copilot/model-multipliers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
multiplier_paid: 1.0
multiplier_free: Not applicable

- name: GPT-5.2
multiplier_paid: 1.0
multiplier_free: Not applicable

- name: Grok Code Fast 1
multiplier_paid: 0.25
multiplier_free: Not applicable
Expand Down
7 changes: 7 additions & 0 deletions data/tables/copilot/model-release-status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@
ask_mode: true
edit_mode: true

- name: 'GPT-5.2'
provider: 'OpenAI'
release_status: 'Public preview'
agent_mode: true
ask_mode: true
edit_mode: true

# Anthropic models
- name: 'Claude Haiku 4.5'
provider: 'Anthropic'
Expand Down
8 changes: 8 additions & 0 deletions data/tables/copilot/model-supported-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@
xcode: false
jetbrains: false

- name: GPT-5.2
dotcom: true
vscode: true
vs: false
eclipse: false
xcode: false
jetbrains: false

- name: Grok Code Fast 1
dotcom: true
vscode: true
Expand Down
7 changes: 7 additions & 0 deletions data/tables/copilot/model-supported-plans.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@
business: true
enterprise: true

- name: GPT-5.2
free: false
pro: true
pro_plus: true
business: true
enterprise: true

- name: Grok Code Fast 1
free: false
pro: true
Expand Down
1 change: 1 addition & 0 deletions data/variables/copilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ copilot_gpt_51: 'GPT-5.1'
copilot_gpt_51_codex: 'GPT-5.1-Codex'
copilot_gpt_51_codex_mini: 'GPT-5.1-Codex-Mini'
copilot_gpt_51_codex_max: 'GPT-5.1-Codex-Max'
copilot_gpt_52: 'GPT-5.2'
# OpenAI 'o' series:
copilot_o3: 'o3'
copilot_o4_mini: 'o4-mini'
Expand Down
30 changes: 30 additions & 0 deletions src/article-api/templates/audit-logs-page.template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# {{ page.title }}

{{ page.intro }}

{{ manualContent }}

## Audit log events

{% for categoryEntry in categorizedEvents %}
{% assign categoryName = categoryEntry[0] %}
{% assign events = categoryEntry[1] %}
### {{ categoryName }}

{% if categoryNotes[categoryName] %}
{{ categoryNotes[categoryName] }}

{% endif %}
{% for event in events %}
#### `{{ event.action }}`

{{ event.description }}

**Fields:** {% if event.fields %}{% for field in event.fields %}`{{ field }}`{% unless forloop.last %}, {% endunless %}{% endfor %}{% else %}No fields available{% endif %}

{% if event.docs_reference_links and event.docs_reference_links != 'N/A' %}
**Reference:** {{ event.docs_reference_links }}
{% endif %}

{% endfor %}
{% endfor %}
95 changes: 95 additions & 0 deletions src/article-api/tests/audit-logs-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { beforeAll, describe, expect, test } from 'vitest'

import { get } from '@/tests/helpers/e2etest'

const makeURL = (pathname: string): string => {
const params = new URLSearchParams({ pathname })
return `/api/article/body?${params}`
}

describe('Audit Logs transformer', () => {
beforeAll(() => {
if (!process.env.ROOT) {
console.warn(
'WARNING: The Audit Logs transformer tests require the ROOT environment variable to be set to the fixture root',
)
}
})

test('Security log events page renders with markdown structure', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')

// Check for the main heading
expect(res.body).toContain('# Security log events')

// Check for intro
expect(res.body).toContain(
'Learn about security log events recorded for your personal account.',
)

// Check for manual content section heading
expect(res.body).toContain('## About security log events')

// Check for new main heading
expect(res.body).toContain('## Audit log events')

// Check for category heading
// The template renders "### Category"
expect(res.body).toMatch(/### \w+/)
})

test('Enterprise audit log events page renders with markdown structure', async () => {
const res = await get(
makeURL(
'/en/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise',
),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')

expect(res.body).toContain('# Audit log events for your enterprise')
})

test('Organization audit log events page renders with markdown structure', async () => {
const res = await get(
makeURL(
'/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization',
),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')

expect(res.body).toContain('# Audit log events for your organization')
})

test('Events are formatted correctly', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)

// Check for event action header
// #### `action.name`
expect(res.body).toMatch(/#### `[\w.]+`/)

// Check for fields section
expect(res.body).toContain('**Fields:**')

// Check for reference section
expect(res.body).toContain('**Reference:**')
})

test('Manual content is preserved', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)

// The source file has manual content before the marker
expect(res.body).toContain('## About security log events')
})
})
139 changes: 139 additions & 0 deletions src/article-api/transformers/audit-logs-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { Context, Page } from '@/types'
import type { PageTransformer } from './types'
import type { CategorizedEvents } from '@/audit-logs/types'
import { renderContent } from '@/content-render/index'
import matter from '@gr2m/gray-matter'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

/**
* Transformer for Audit Logs pages
* Converts audit log events and their data into markdown format using a Liquid template
*/
export class AuditLogsTransformer implements PageTransformer {
canTransform(page: Page): boolean {
return page.autogenerated === 'audit-logs'
}

async transform(page: Page, pathname: string, context: Context): Promise<string> {
// Import audit log lib dynamically to avoid circular dependencies
const { getCategorizedAuditLogEvents, getCategoryNotes, resolveReferenceLinksToMarkdown } =
await import('@/audit-logs/lib/index')

// Extract version from context
const currentVersion = context.currentVersion!

let pageType = ''
if (pathname.includes('/security-log-events')) {
pageType = 'user'
} else if (pathname.includes('/audit-log-events-for-your-enterprise')) {
pageType = 'enterprise'
} else if (pathname.includes('/audit-log-events-for-your-organization')) {
pageType = 'organization'
} else {
throw new Error(`Unknown audit log page type for path: ${pathname}`)
}

// Get the audit log events data
const categorizedEvents = getCategorizedAuditLogEvents(pageType, currentVersion)
const categoryNotes = getCategoryNotes()

// Prepare manual content
let manualContent = ''
if (page.markdown) {
const markerIndex = page.markdown.indexOf(
'<!-- Content after this section is automatically generated -->',
)
if (markerIndex > 0) {
const { content } = matter(page.markdown)
const manualContentMarkerIndex = content.indexOf(
'<!-- Content after this section is automatically generated -->',
)
if (manualContentMarkerIndex > 0) {
const rawManualContent = content.substring(0, manualContentMarkerIndex).trim()
if (rawManualContent) {
manualContent = await renderContent(rawManualContent, {
...context,
markdownRequested: true,
})
}
}
}
}

// Prepare data for template
const templateData = await this.prepareTemplateData(
page,
categorizedEvents,
categoryNotes,
context,
manualContent,
resolveReferenceLinksToMarkdown,
)

// Load and render template
const templatePath = join(__dirname, '../templates/audit-logs-page.template.md')
const templateContent = readFileSync(templatePath, 'utf8')

// Render the template with Liquid
const rendered = await renderContent(templateContent, {
...context,
...templateData,
markdownRequested: true,
})

return rendered
}

/**
* Prepare data for the Liquid template
*/
private async prepareTemplateData(
page: Page,
categorizedEvents: CategorizedEvents,
categoryNotes: Record<string, string>,
context: Context,
manualContent: string,
resolveReferenceLinksToMarkdown: (docsReferenceLinks: string, context: any) => Promise<string>,
): Promise<Record<string, unknown>> {
// Prepare page intro
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''

// Sort categories and events
const sortedCategorizedEvents: CategorizedEvents = {}
const sortedCategories = Object.keys(categorizedEvents).sort((a, b) => a.localeCompare(b))

for (const category of sortedCategories) {
// Create a copy of the events array to avoid mutating the cache
const events = [...categorizedEvents[category]].sort((a, b) =>
a.action.localeCompare(b.action),
)
sortedCategorizedEvents[category] = await Promise.all(
events.map(async (event) => {
const newEvent = { ...event }
if (newEvent.docs_reference_links && newEvent.docs_reference_links !== 'N/A') {
newEvent.docs_reference_links = await resolveReferenceLinksToMarkdown(
newEvent.docs_reference_links,
context,
)
}
return newEvent
}),
)
}

return {
page: {
title: page.title,
intro,
},
manualContent,
categorizedEvents: sortedCategorizedEvents,
categoryNotes,
}
}
}
9 changes: 2 additions & 7 deletions src/article-api/transformers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TransformerRegistry } from './types'
import { RestTransformer } from './rest-transformer'
import { AuditLogsTransformer } from './audit-logs-transformer'
import { GraphQLTransformer } from './graphql-transformer'

/**
Expand All @@ -8,15 +9,9 @@ import { GraphQLTransformer } from './graphql-transformer'
*/
export const transformerRegistry = new TransformerRegistry()

// Register REST transformer
transformerRegistry.register(new RestTransformer())

// Register GraphQL transformer
transformerRegistry.register(new AuditLogsTransformer())
transformerRegistry.register(new GraphQLTransformer())

// Future transformers can be registered here:
// transformerRegistry.register(new WebhooksTransformer())
// transformerRegistry.register(new GitHubAppsTransformer())

export { TransformerRegistry } from './types'
export type { PageTransformer } from './types'
Loading
Loading