1
0
mirror of synced 2026-01-08 12:01:53 -05:00

Merge remote-tracking branch 'private/main'

This commit is contained in:
Kevin Heis
2025-05-19 17:41:21 -07:00
23 changed files with 333 additions and 179 deletions

View File

@@ -48,6 +48,7 @@ jobs:
# will be checked out
repository: github/models-gateway
path: models-gateway
ref: main
- uses: ./.github/actions/node-npm-setup

View File

@@ -47,9 +47,22 @@ jobs:
- uses: ./.github/actions/node-npm-setup
- name: Check issue exists
id: exists
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_URL: ${{ github.event.issue.html_url }}
run: |
if gh issue view $ISSUE_URL > /dev/null 2>&1
then
echo "exists=y" >> $GITHUB_OUTPUT
else
echo "exists=n" >> $GITHUB_OUTPUT
fi
- name: Label issues with new comments with 'triage'
uses: ./.github/actions/labeler
if: ${{ steps.is-internal-contributor.outputs.result == 'false' }}
if: ${{ steps.is-internal-contributor.outputs.result == 'false' && steps.exists.outputs.exists == 'y' }}
with:
addLabels: 'triage'
ignoreIfLabeled: true

View File

@@ -47,8 +47,13 @@ For information about {% data variables.product.prodname_code_scanning %} alerts
{% data variables.product.prodname_codeql %} supports both compiled and interpreted languages, and can find vulnerabilities and errors in code that's written in the supported languages.
{% data variables.product.prodname_codeql %} supports the following languages:
{% data reusables.code-scanning.codeql-languages-bullets %}
> [!IMPORTANT]
> {% data variables.product.prodname_codeql %} does **not** support languages that are not listed above. This includes, but is not limited to, **Rust**, **PHP**, **Scala**, and others. Attempting to use {% data variables.product.prodname_codeql %} with unsupported languages may result in no alerts being generated and incomplete analysis.
## Modeling custom or niche frameworks
{% data variables.product.github %} experts, security researchers, and community contributors write libraries to model the flow of data in popular frameworks and libraries. If you use custom dependencies that aren't modeled, then you can use the {% data variables.product.prodname_codeql %} extension for {% data variables.product.prodname_vscode %} to create models for these dependencies and use them to extend your analysis. For more information, see [AUTOTITLE](/code-security/codeql-for-vs-code/using-the-advanced-functionality-of-the-codeql-for-vs-code-extension/using-the-codeql-model-editor).

View File

@@ -17,7 +17,7 @@ topics:
The {% data variables.product.prodname_github_security_configuration %} is a collection of enablement settings for {% data variables.product.company_short %}'s security features that is created and maintained by subject matter experts at {% data variables.product.company_short %}. The {% data variables.product.prodname_github_security_configuration %} is designed to successfully reduce the security risks for low- and high-impact repositories. We recommend you apply this configuration to all the repositories in your organization.
> [!NOTE]
> The {% data variables.product.prodname_github_security_configuration %} includes {% data variables.product.prodname_GH_code_security %} and {% data variables.product.prodname_GH_secret_protection %} features. Applying the configuration to private and internal repositories in your organization will incur usage costs or require licenses.
> The {% data variables.product.prodname_github_security_configuration %} includes {% data variables.product.prodname_GH_code_security %} and {% data variables.product.prodname_GH_secret_protection %} features. Applying the configuration to repositories in your organization will incur usage costs or require licenses.
## Applying the {% data variables.product.prodname_github_security_configuration %} to all repositories in your organization

View File

@@ -31,7 +31,7 @@ The {% data variables.product.prodname_github_security_configuration %} offers a
* It is the quickest {% data variables.product.prodname_security_configuration %} to apply to all repositories in your organization.
* It is designed to effectively secure both low- and high-impact repositories.
The {% data variables.product.prodname_github_security_configuration %} includes {% data variables.product.prodname_GH_code_security %} and {% data variables.product.prodname_GH_secret_protection %} features. Applying the configuration to private and internal repositories in your organization will incur usage costs or require licenses.
The {% data variables.product.prodname_github_security_configuration %} includes {% data variables.product.prodname_GH_code_security %} and {% data variables.product.prodname_GH_secret_protection %} features. Applying the configuration to repositories in your organization will incur usage costs or require licenses.
To start securing repositories in your organization with the {% data variables.product.prodname_github_security_configuration %}, see [AUTOTITLE](/code-security/securing-your-organization/enabling-security-features-in-your-organization/applying-the-github-recommended-security-configuration-in-your-organization).

View File

@@ -265,7 +265,7 @@ Low, high, and embedding models have different rate limits. To see which type of
<td>1</td>
</tr>
<tr>
<th rowspan="4" scope="rowgroup"><b>Azure OpenAI o1 and o3</b></th>
<th rowspan="4" scope="rowgroup"><b>Azure OpenAI o1 and o3, xAI Grok-3</b></th>
<th style="padding-left: 0"><b>Requests per minute</b></th>
<td>Not applicable</td>
<td>1</td>

View File

@@ -18,27 +18,36 @@ Prompts can be stored as files directly within GitHub repositories. This unlocks
## Supported file format
Store prompts in markdown files with optional YAML front matter.
Store prompts in YAML files.
The file can be located anywhere in your repository, but it **must have the extension `.prompt.md`**.
The file can be located anywhere in your repository, but _must have the extension `.prompt.yml` or `.prompt.yaml`._
Example:
```yaml
---
name: Summarizer
description: Summarizes a given text
model: openai/gpt-4o
model_parameters:
``` yaml copy
name: Text Summarizer
description: Summarizes input text concisely
model: gpt-4o-mini
modelParameters:
temperature: 0.5
---
system:
You are a text summarizer. Your only job is to summarize a given text to you.
user:
Summarize the given text:
<text>
{% raw %}{{text}}{% endraw %}
</text>
messages:
- role: system
content: You are a text summarizer. Your only job is to summarize text given to you.
- role: user
content: |
Summarize the given text, beginning with "Summary -":
<text>
{% raw %}{{input}}{% endraw %}
</text>
testData:
- input: |
The quick brown fox jumped over the lazy dog.
The dog was too tired to react.
expected: Summary - A fox jumped over a lazy, unresponsive dog.
evaluators:
- name: Output should start with 'Summary -'
string:
startsWith: 'Summary -'
```
## Prompt structure
@@ -46,11 +55,12 @@ Summarize the given text:
Prompts have two key parts:
* **Runtime information** (required)
* Prompt templates (system, user, etc.) using simple {{variable}} placeholders
* Prompt templates (system, user, etc.) using simple {% raw %}`{{variable}}`{% endraw %} placeholders
* **Development information** (optional)
* Human-readable name and description
* Model identifier and parameters
* Sample data for testing and evaluations
* Data describing the evaluators themselves
## Limitations

View File

@@ -47,7 +47,7 @@ search:
privacy_disclaimer: For product and service improvement purposes, the GitHub Docs team will retain questions and answers generated in the Docs search function. Please see the <a href="https://docs.github.com/privacy"><u>GitHub Privacy Statement</u></a> to review how GitHub collects and uses your data.
ai:
disclaimer: <a href="https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-githubcom"}>Copilot</a> uses AI. Check for mistakes.
references: Additional docs
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
copy_answer: Copy answer

View File

@@ -27,7 +27,7 @@
"content-changes-table-comment": "tsx src/workflows/content-changes-table-comment.ts",
"copy-fixture-data": "tsx src/tests/scripts/copy-fixture-data.js",
"count-translation-corruptions": "tsx src/languages/scripts/count-translation-corruptions.ts",
"create-enterprise-issue": "tsx src/ghes-releases/scripts/create-enterprise-issue.js",
"create-enterprise-issue": "tsx src/ghes-releases/scripts/create-enterprise-issue.ts",
"debug": "cross-env NODE_ENV=development ENABLED_LANGUAGES=en nodemon --inspect src/frame/server.ts",
"delete-orphan-translation-files": "tsx src/workflows/delete-orphan-translation-files.ts",
"deleted-assets-pr-comment": "tsx src/assets/scripts/deleted-assets-pr-comment.ts",
@@ -35,6 +35,7 @@
"deprecate-ghes": "tsx src/ghes-releases/scripts/deprecate/index.ts",
"deprecate-ghes-archive": "cross-env NODE_OPTIONS=--max-old-space-size=16384 tsx src/ghes-releases/scripts/deprecate/archive-version.ts",
"dev": "cross-env npm start",
"dev-toc": "tsx src/dev-toc/generate.ts",
"enable-automerge": "tsx src/workflows/enable-automerge.ts",
"find-orphaned-assets": "tsx src/assets/scripts/find-orphaned-assets.ts",
"find-orphaned-features": "tsx src/data-directory/scripts/find-orphaned-features/index.ts",
@@ -75,7 +76,7 @@
"purge-fastly-edge-cache-per-language": "tsx src/languages/scripts/purge-fastly-edge-cache-per-language.js",
"purge-old-workflow-runs": "tsx src/workflows/purge-old-workflow-runs.js",
"ready-for-docs-review": "tsx src/workflows/ready-for-docs-review.ts",
"release-banner": "tsx src/ghes-releases/scripts/release-banner.js",
"release-banner": "tsx src/ghes-releases/scripts/release-banner.ts",
"reusables": "tsx src/content-render/scripts/reusables-cli.ts",
"rendered-content-link-checker": "tsx src/links/scripts/rendered-content-link-checker.ts",
"rendered-content-link-checker-cli": "tsx src/links/scripts/rendered-content-link-checker-cli.ts",
@@ -97,7 +98,7 @@
"tsc": "tsc --noEmit",
"unallowed-contributions": "tsx src/workflows/unallowed-contributions.ts",
"update-data-and-image-paths": "tsx src/early-access/scripts/update-data-and-image-paths.js",
"update-enterprise-dates": "tsx src/ghes-releases/scripts/update-enterprise-dates.js",
"update-enterprise-dates": "tsx src/ghes-releases/scripts/update-enterprise-dates.ts",
"update-internal-links": "tsx src/links/scripts/update-internal-links.ts",
"validate-asset-images": "tsx src/assets/scripts/validate-asset-images.ts",
"validate-github-github-docs-urls": "tsx src/links/scripts/validate-github-github-docs-urls/index.ts",

View File

@@ -20,7 +20,7 @@ describe('audit log events docs', () => {
path: '/authentication/keeping-your-account-and-data-secure/security-log-events',
type: 'user',
},
]
] as const
// This test ensures that the audit log event page components and Markdown
// file are in sync. Additionally, it checks all event categories are
@@ -32,7 +32,7 @@ describe('audit log events docs', () => {
// the enterprise events page has no FPT versioned audit log data
if (page.type === 'enterprise' && version === 'free-pro-team@latest') continue
const auditLogEvents = getCategorizedAuditLogEvents(page.type, version, true)
const auditLogEvents = getCategorizedAuditLogEvents(page.type, version)
if (Object.keys(auditLogEvents).length === 0) {
console.warn(`There are no audit log events for ${page.path} with version '${version}'.`)

View File

@@ -1,70 +1,90 @@
import { describe, expect, test } from 'vitest'
import { filterByAllowlistValues, filterAndUpdateGhesDataByAllowlistValues } from '../../lib'
import type { RawAuditLogEventT, VersionedAuditLogData } from '../../types'
describe('audit log event fitering', () => {
test('matches single allowlist value', () => {
const eventsToProcess = [
const eventsToProcess: RawAuditLogEventT[] = [
{
action: 'repo.create',
_allowlists: ['user'],
description: 'repo was created',
docs_reference_links: '',
ghes: {},
},
]
const filteredEvents = filterByAllowlistValues(eventsToProcess, 'user')
const filteredEvents = filterByAllowlistValues(eventsToProcess, 'user', [], {
sha: '',
appendedDescriptions: {},
})
expect(filteredEvents[0].action).toEqual('repo.create')
})
test('matches multiple allowlist values', () => {
const eventsToProcess = [
const eventsToProcess: RawAuditLogEventT[] = [
{
action: 'repo.create',
_allowlists: ['user'],
description: 'repo was created',
docs_reference_links: '',
ghes: {},
},
{
action: 'repo.delete',
_allowlists: ['organization'],
description: 'repo was deleted',
docs_reference_links: '',
ghes: {},
},
]
const filteredEvents = filterByAllowlistValues(eventsToProcess, ['user', 'organization'])
const filteredEvents = filterByAllowlistValues(eventsToProcess, ['user', 'organization'], [], {
sha: '',
appendedDescriptions: {},
})
expect(filteredEvents[0].action).toEqual('repo.create')
expect(filteredEvents.length).toEqual(2)
})
test('does not match non-matching allowlist value', () => {
const eventsToProcess = [
const eventsToProcess: RawAuditLogEventT[] = [
{
action: 'repo.create',
_allowlists: ['user'],
description: 'repo was created',
docs_reference_links: '',
ghes: {},
},
]
const filteredEvents = filterByAllowlistValues(eventsToProcess, 'organization')
const filteredEvents = filterByAllowlistValues(eventsToProcess, 'organization', [], {
sha: '',
appendedDescriptions: {},
})
expect(filteredEvents.length).toBe(0)
})
test('ghes filters and updates multiple ghes versions', () => {
const eventsToProcess = [
const eventsToProcess: RawAuditLogEventT[] = [
{
action: 'repo.create',
description: 'repo was created',
docs_reference_links: '',
_allowlists: [],
ghes: {
'3.10': {
_allowlists: ['user'],
},
3.11: {
'3.11': {
_allowlists: ['user'],
},
},
},
]
const currentEvents = {
const currentEvents: VersionedAuditLogData = {
'ghes-3.11': {
organization: [
{
@@ -90,10 +110,13 @@ describe('audit log event fitering', () => {
eventsToProcess,
'user',
currentEvents,
{},
{
sha: '',
appendedDescriptions: {},
},
auditLogPage,
)
const getActions = (version) =>
const getActions = (version: string) =>
currentEvents[version][auditLogPage].map((event) => event.action)
expect(getActions('ghes-3.10').includes('repo.create')).toBe(true)
expect(getActions('ghes-3.11').includes('repo.create')).toBe(true)

View File

@@ -21,7 +21,7 @@ export const EXPERIMENTS = {
ai_search_experiment: {
key: 'ai_search_experiment',
isActive: true, // Set to false when the experiment is over
percentOfUsersToGetExperiment: 20, // 20% of users will get the experiment
percentOfUsersToGetExperiment: 30, // 30% of users will get the experiment
includeVariationInContext: true, // All events will include the `experiment_variation` of the `ai_search_experiment`
limitToLanguages: ['en'], // Only users with the `en` language will be included in the experiment
alwaysShowForStaff: true, // When set to true, staff will always see the experiment (determined by the `staffonly` cookie)

View File

@@ -47,7 +47,7 @@ search:
privacy_disclaimer: For product and service improvement purposes, the GitHub Docs team will retain questions and answers generated in the Docs search function. Please see the <a href="https://docs.github.com/privacy"><u>GitHub Privacy Statement</u></a> to review how GitHub collects and uses your data.
ai:
disclaimer: <a href="https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-githubcom"}>Copilot</a> uses AI. Check for mistakes.
references: Additional docs
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
copy_answer: Copy answer

View File

@@ -8,6 +8,39 @@ import { latest, oldestSupported } from '#src/versions/lib/enterprise-server-rel
import { getContents } from '#src/workflows/git-utils.ts'
import github from '#src/workflows/github.ts'
interface ReleaseDates {
[releaseNumber: string]: {
start: string
end: string
prp?: string
feature_freeze?: string
code_freeze?: string
release_candidate?: string
}
}
interface ReleaseTemplate {
content: string
issue?: {
number: number
html_url: string
}
}
interface ReleaseTemplates {
[templateName: string]: ReleaseTemplate
}
interface ReleaseTemplateContext {
[key: string]: string
}
interface IssueSearchOpts {
labels?: string[]
searchQuery?: string
titleMatch?: string
}
// Required by github() to authenticate
if (!process.env.GITHUB_TOKEN) {
throw new Error('Error! You must have a GITHUB_TOKEN set in an .env file to run this script.')
@@ -99,7 +132,7 @@ async function createReleaseIssue() {
// Only open an issue if today is within 30 days before
// the release candidate date
if (getNumberDaysUntilMilestone(rcDate) > 30) {
if (getNumberDaysUntilMilestone(rcDate || '') > 30) {
console.log(
`The ${releaseNumber} release candidate is not until ${rcDate}! An issue will be opened 30 days prior to the release candidate date.`,
)
@@ -134,11 +167,18 @@ async function createReleaseIssue() {
releaseTemplateContext,
)
await addRepoLabels(repo, labels)
await updateIssue(repo, template.issue.number, title, body, labels)
await updateIssue(repo, template.issue!.number, title, body, labels)
}
}
async function createIssue(fullRepo, title, body, labels, releaseNumber, releaseType) {
async function createIssue(
fullRepo: string,
title: string,
body: string,
labels: string[],
releaseNumber: string,
releaseType: string,
) {
const [owner, repo] = fullRepo.split('/')
if (!owner || !repo) throw new Error('Please provide a valid repo name in the format owner/repo')
let issue
@@ -150,7 +190,7 @@ async function createIssue(fullRepo, title, body, labels, releaseNumber, release
body,
labels,
})
} catch (error) {
} catch (error: any) {
console.log(`#ERROR# ${error}\n🛑 There was an error creating the issue.`)
throw error
}
@@ -163,7 +203,13 @@ async function createIssue(fullRepo, title, body, labels, releaseNumber, release
return issue
}
async function updateIssue(fullRepo, issueNumber, title, body, labels) {
async function updateIssue(
fullRepo: string,
issueNumber: number,
title: string,
body: string,
labels: string[],
) {
const [owner, repo] = fullRepo.split('/')
if (!owner || !repo) throw new Error('Please provide a valid repo name in the format owner/repo')
@@ -177,7 +223,7 @@ async function updateIssue(fullRepo, issueNumber, title, body, labels) {
body,
labels,
})
} catch (error) {
} catch (error: any) {
console.log(
`#ERROR# ${error}\n🛑 There was an error updating issue ${issueNumber} in ${fullRepo}.`,
)
@@ -188,9 +234,9 @@ async function updateIssue(fullRepo, issueNumber, title, body, labels) {
}
}
async function addRepoLabels(fullRepo, labels) {
async function addRepoLabels(fullRepo: string, labels: string[]) {
const [owner, repo] = fullRepo.split('/')
const labelsToAdd = []
const labelsToAdd: string[] = []
for (const name of labels) {
try {
await octokit.request('GET /repos/{owner}/{repo}/labels/{name}', {
@@ -198,7 +244,7 @@ async function addRepoLabels(fullRepo, labels) {
repo,
name,
})
} catch (error) {
} catch (error: any) {
if (error.status === 404) {
labelsToAdd.push(name)
} else {
@@ -214,65 +260,78 @@ async function addRepoLabels(fullRepo, labels) {
repo,
name,
})
} catch (error) {
} catch (error: any) {
console.log(`#ERROR# ${error}\n🛑 There was an error adding the label ${name}.`)
throw error
}
}
}
function getReleaseTemplates() {
function getReleaseTemplates(): ReleaseTemplates {
const templateFiles = walk('src/ghes-releases/lib/release-templates', {
includeBasePath: true,
directories: false,
globs: ['**/*.md'],
ignore: ['**/README.md'],
})
const releaseTemplates = {}
const releaseTemplates: ReleaseTemplates = {}
for (const file of templateFiles) {
releaseTemplates[basename(file, '.md')] = { content: readFileSync(file, 'utf8') }
}
return releaseTemplates
}
function getReleaseTemplateContext(releaseNumber, releaseInfo, releaseTemplates) {
const context = {
function getReleaseTemplateContext(
releaseNumber: string,
releaseInfo: ReleaseDates[string],
releaseTemplates: ReleaseTemplates,
): ReleaseTemplateContext {
const context: ReleaseTemplateContext = {
'release-number': releaseNumber,
'release-target-date': releaseInfo.start,
'release-prp': releaseInfo.prp,
'release-feature-freeze-date': releaseInfo.feature_freeze,
'release-code-freeze-date': releaseInfo.code_freeze,
'release-rc-target-date': releaseInfo.release_candidate,
'release-prp': releaseInfo.prp || '',
'release-feature-freeze-date': releaseInfo.feature_freeze || '',
'release-code-freeze-date': releaseInfo.code_freeze || '',
'release-rc-target-date': releaseInfo.release_candidate || '',
}
// Add a context variable for each issue url
for (const [templateName, template] of Object.entries(releaseTemplates)) {
context[`${templateName}-url`] = template.issue.html_url
if (template.issue) {
context[`${templateName}-url`] = template.issue.html_url
}
}
// Create a context variable for each of the
// 7 days before release-rc-target-date
const rcTargetDate = new Date(releaseInfo.release_candidate).getTime()
for (let i = 1; i <= 7; i++) {
const day = i
const milliSecondsBefore = day * 24 * 60 * 60 * 1000
const rcDateBefore = new Date(rcTargetDate - milliSecondsBefore).toISOString().slice(0, 10)
context[`release-rc-target-date-minus-${day}`] = rcDateBefore
if (releaseInfo.release_candidate) {
const rcTargetDate = new Date(releaseInfo.release_candidate).getTime()
for (let i = 1; i <= 7; i++) {
const day = i
const milliSecondsBefore = day * 24 * 60 * 60 * 1000
const rcDateBefore = new Date(rcTargetDate - milliSecondsBefore).toISOString().slice(0, 10)
context[`release-rc-target-date-minus-${day}`] = rcDateBefore
}
}
return context
}
async function getRenderedTemplate(templateContent, releaseTemplateContext) {
async function getRenderedTemplate(
templateContent: string,
releaseTemplateContext: ReleaseTemplateContext,
) {
const { content, data } = matter(templateContent)
const title = await liquid.parseAndRender(data.title, releaseTemplateContext)
const body = await liquid.parseAndRender(content, releaseTemplateContext)
const labels = await Promise.all(
data.labels.map(async (label) => await liquid.parseAndRender(label, releaseTemplateContext)),
data.labels.map(
async (label: string) => await liquid.parseAndRender(label, releaseTemplateContext),
),
)
return { title, body, labels }
}
function getNumberDaysUntilMilestone(milestoneDate) {
function getNumberDaysUntilMilestone(milestoneDate: string): number {
const today = new Date().toISOString().slice(0, 10)
const nextMilestoneDateTime = new Date(milestoneDate).getTime()
const todayTime = new Date(today).getTime()
@@ -281,7 +340,7 @@ function getNumberDaysUntilMilestone(milestoneDate) {
return Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24))
}
function getNextReleaseNumber(releaseDates) {
function getNextReleaseNumber(releaseDates: ReleaseDates): string {
const indexOfLatest = Object.keys(releaseDates).indexOf(latest)
const indexOfNext = indexOfLatest + 1
return Object.keys(releaseDates)[indexOfNext]
@@ -292,9 +351,9 @@ function getNextReleaseNumber(releaseDates) {
// labels: ['enterprise deprecation', 'ghes 3.0']
// titleMatch: 'GHES 3.0'
async function isExistingIssue(
repo,
opts = { labels: undefined, searchQuery: undefined, titleMatch: undefined },
) {
repo: string,
opts: IssueSearchOpts = { labels: undefined, searchQuery: undefined, titleMatch: undefined },
): Promise<boolean> {
const { labels, searchQuery, titleMatch } = opts
const labelQuery = labels && labels.map((label) => `label:"${encodeURI(label)}"`).join('+')
let query = encodeURIComponent('is:issue ' + `repo:${repo} `)
@@ -314,21 +373,21 @@ async function isExistingIssue(
console.log(`Issue ${issue.html_url} already exists for this release.`)
return true
}
return
return false
}
}
const issueExists = !!issues.data.items.length
if (issueExists) {
console.log(
`Issue ${issues.data.items.map((item) => item.html_url)} already exists for this release.`,
`Issue ${issues.data.items.map((item: { html_url: string }) => item.html_url)} already exists for this release.`,
)
}
return issueExists
}
async function getReleaseDates() {
let rawDates = []
async function getReleaseDates(): Promise<ReleaseDates> {
let rawDates: ReleaseDates = {}
try {
rawDates = JSON.parse(
await getContents('github', 'enterprise-releases', 'master', 'releases.json'),

View File

@@ -50,7 +50,7 @@ gh api -X POST "/repos/github/docs-ghes-$version/pages" \
echo "--- Update custom properties"
gh api --method PATCH /repos/github/docs-ghes-$version/properties/values \
-f "properties[][property_name]=ownership-name" \
-f "properties[][value]=@github/docs-engineering" \
-f "properties[][value]=@github/docs" \
-f "properties[][property_name]=ownership-type" \
-f "properties[][value]=Team" \
--silent
@@ -74,4 +74,4 @@ git push
cd ..
echo "--- END FILE UPDATES"
echo "--- MANUAL NOTES"
echo "Manually disable releases, packages, and deployments"
echo "Manually disable releases, packages, and deployments"

View File

@@ -10,7 +10,9 @@ import { program } from 'commander'
import { allVersions } from '#src/versions/lib/all-versions.js'
const releaseCandidateJSFile = 'src/versions/lib/enterprise-server-releases.js'
const allowedActions = ['create', 'remove']
const allowedActions = ['create', 'remove'] as const
type AllowedAction = (typeof allowedActions)[number]
program
.description('Create or remove a release candidate banner for the provided docs version.')
@@ -23,7 +25,7 @@ program
const options = program.opts()
if (!allowedActions.includes(options.action)) {
if (!allowedActions.includes(options.action as AllowedAction)) {
console.log(`Error! You must specify --action <${allowedActions.join(' or ')}>.`)
process.exit(1)
}
@@ -36,30 +38,37 @@ if (!Object.keys(allVersions).includes(options.version)) {
}
// Load the release candidate variable
let jsCode = await fs.readFile(releaseCandidateJSFile, 'utf8')
const lineRegex = /export const releaseCandidate = .*/
if (!lineRegex.test(jsCode)) {
throw new Error(
`The file ${releaseCandidateJSFile} does not contain a release candidate variable. ('export const releaseCandidate = ...')`,
)
}
async function main(): Promise<void> {
let jsCode = await fs.readFile(releaseCandidateJSFile, 'utf8')
const lineRegex = /export const releaseCandidate = .*/
if (!lineRegex.test(jsCode)) {
throw new Error(
`The file ${releaseCandidateJSFile} does not contain a release candidate variable. ('export const releaseCandidate = ...')`,
)
}
// Create or remove the variable
if (options.action === 'create') {
jsCode = jsCode.replace(
lineRegex,
`export const releaseCandidate = '${options.version.split('@')[1]}'`,
)
} else if (options.action === 'remove') {
jsCode = jsCode.replace(lineRegex, `export const releaseCandidate = null`)
}
// Create or remove the variable
if (options.action === 'create') {
jsCode = jsCode.replace(
lineRegex,
`export const releaseCandidate = '${options.version.split('@')[1]}'`,
)
} else if (options.action === 'remove') {
jsCode = jsCode.replace(lineRegex, `export const releaseCandidate = null`)
}
// Update the file
await fs.writeFile(releaseCandidateJSFile, jsCode)
// Update the file
await fs.writeFile(releaseCandidateJSFile, jsCode)
// Display next steps
console.log(`\nDone! Commit the update to ${releaseCandidateJSFile}. This ${options.action}s the banner for ${options.version}.
// Display next steps
console.log(`\nDone! Commit the update to ${releaseCandidateJSFile}. This ${options.action}s the banner for ${options.version}.
- To change the banner text, you can edit header.notices.release_candidate in data/ui.yml.
- To change the banner styling, you can edit includes/header-notification.html.
`)
}
main().catch((error) => {
console.error('Error:', error)
process.exit(1)
})

View File

@@ -11,6 +11,21 @@ import fs from 'fs/promises'
import { getContents } from '#src/workflows/git-utils.ts'
interface EnterpriseDates {
[releaseNumber: string]: {
releaseDate: string
deprecationDate: string
}
}
interface RawReleaseData {
[releaseNumber: string]: {
release_candidate?: string
start: string
end: string
}
}
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const enterpriseDatesFile = path.join(__dirname, '../lib/enterprise-dates.json')
const enterpriseDatesString = await fs.readFile(enterpriseDatesFile, 'utf8')
@@ -22,9 +37,9 @@ if (!process.env.GITHUB_TOKEN) {
main()
async function main() {
async function main(): Promise<void> {
// send owner, repo, ref, path
let rawDates = []
let rawDates: RawReleaseData = {}
try {
rawDates = JSON.parse(
await getContents('github', 'enterprise-releases', 'master', 'releases.json'),
@@ -36,7 +51,7 @@ async function main() {
throw error
}
const formattedDates = {}
const formattedDates: EnterpriseDates = {}
Object.entries(rawDates).forEach(([releaseNumber, releaseObject]) => {
formattedDates[releaseNumber] = {
releaseDate: releaseObject.release_candidate || releaseObject.start,

View File

@@ -4,7 +4,14 @@ import { executeAISearch } from '../helpers/execute-search-actions'
import { useRouter } from 'next/router'
import { useTranslation } from '@/languages/components/useTranslation'
import { ActionList, IconButton, Spinner } from '@primer/react'
import { CheckIcon, CopyIcon, FileIcon, ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react'
import {
CheckIcon,
CopilotIcon,
CopyIcon,
FileIcon,
ThumbsdownIcon,
ThumbsupIcon,
} from '@primer/octicons-react'
import { announce } from '@primer/live-region-element'
import { useAISearchLocalStorageCache } from '../hooks/useAISearchLocalStorageCache'
import { UnrenderedMarkdownContent } from '@/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent'
@@ -334,6 +341,64 @@ export function AskAIResults({
return (
<div className={styles.container}>
{!aiCouldNotAnswer && references && references.length > 0 ? (
<>
<ActionList className={styles.referencesList} showDividers>
<ActionList.Group>
<ActionList.GroupHeading
as="h3"
aria-label={t('search.ai.references')}
className={styles.referencesTitle}
>
{t('search.ai.references')}
</ActionList.GroupHeading>
{references
.map((source, index) => {
if (index >= MAX_REFERENCES_TO_SHOW) {
return null
}
const refIndex = index + referencesIndexOffset
return (
<ActionList.Item
sx={{
marginLeft: '0px',
}}
key={`reference-${index}`}
id={`search-option-reference-${index + referencesIndexOffset}`}
role="option"
tabIndex={-1}
onSelect={() => {
referenceOnSelect(source.url)
}}
active={refIndex === selectedIndex}
ref={(element) => {
if (listElementsRef.current) {
listElementsRef.current[refIndex] = element
}
}}
>
<ActionList.LeadingVisual aria-hidden="true">
<FileIcon />
</ActionList.LeadingVisual>
{source.title}
</ActionList.Item>
)
})
.filter(Boolean)}
</ActionList.Group>
<ActionList.Divider aria-hidden="true" />
</ActionList>
</>
) : null}
<ActionList.GroupHeading
key="ai-heading"
as="h3"
tabIndex={-1}
aria-label={t('search.overlay.ai_suggestions_list_aria_label')}
>
<CopilotIcon className="mr-1" />
{t('search.overlay.ai_autocomplete_list_heading')}
</ActionList.GroupHeading>
{initialLoading ? (
<div className={styles.loadingContainer} role="status">
<Spinner />
@@ -423,55 +488,6 @@ export function AskAIResults({
></IconButton>
</div>
) : null}
{!aiCouldNotAnswer && references && references.length > 0 ? (
<>
<ActionList.Divider aria-hidden="true" />
<ActionList className={styles.referencesList} showDividers>
<ActionList.Group>
<ActionList.GroupHeading
as="h3"
aria-label={t('search.ai.references')}
className={styles.referencesTitle}
>
{t('search.ai.references')}
</ActionList.GroupHeading>
{references
.map((source, index) => {
if (index >= MAX_REFERENCES_TO_SHOW) {
return null
}
const refIndex = index + referencesIndexOffset
return (
<ActionList.Item
sx={{
marginLeft: '0px',
}}
key={`reference-${index}`}
id={`search-option-reference-${index + referencesIndexOffset}`}
role="option"
tabIndex={-1}
onSelect={() => {
referenceOnSelect(source.url)
}}
active={refIndex === selectedIndex}
ref={(element) => {
if (listElementsRef.current) {
listElementsRef.current[refIndex] = element
}
}}
>
<ActionList.LeadingVisual aria-hidden="true">
<FileIcon />
</ActionList.LeadingVisual>
{source.title}
</ActionList.Item>
)
})
.filter(Boolean)}
</ActionList.Group>
</ActionList>
</>
) : null}
<div
aria-live="assertive"
style={{

View File

@@ -879,23 +879,10 @@ function renderSearchGroups(
) {
const groups = []
const askAIGroupHeading = (
<ActionList.GroupHeading
key="ai-heading"
as="h3"
tabIndex={-1}
aria-label={t('search.overlay.ai_suggestions_list_aria_label')}
>
<CopilotIcon className="mr-1" />
{t('search.overlay.ai_autocomplete_list_heading')}
</ActionList.GroupHeading>
)
let isInAskAIState = askAIState?.isAskAIState && !askAIState.aiSearchError
if (isInAskAIState) {
groups.push(
<ActionList.Group key="ai" data-testid="ask-ai">
{askAIGroupHeading}
<AskAIResults
query={askAIState.aiQuery}
debug={askAIState.debug}

View File

@@ -7,32 +7,44 @@ export const WEBHOOK_DATA_DIR = 'src/webhooks/data'
export const WEBHOOK_SCHEMA_FILENAME = 'schema.json'
// cache for webhook data per version
const webhooksCache = new Map()
const webhooksCache = new Map<string, Promise<Record<string, any>>>()
// cache for webhook data for when you first visit the webhooks page where we
// show all webhooks for the current version but only 1 action type per webhook
// and also no nested parameters
const initialWebhooksCache = new Map()
const initialWebhooksCache = new Map<string, InitialWebhook[]>()
interface InitialWebhook {
name: string
actionTypes: string[]
data: {
bodyParameters?: Array<{
childParamsGroups?: any[]
[key: string]: any
}>
[key: string]: any
}
}
// return the webhoook data as described for `initialWebhooksCache` for the given
// version
export async function getInitialPageWebhooks(version) {
export async function getInitialPageWebhooks(version: string): Promise<InitialWebhook[]> {
if (initialWebhooksCache.has(version)) {
return initialWebhooksCache.get(version)
return initialWebhooksCache.get(version) || []
}
const allWebhooks = await getWebhooks(version)
const initialWebhooks = []
const initialWebhooks: InitialWebhook[] = []
// The webhooks page shows all webhooks but for each webhook only a single
// webhook action type at a time. We pick the first webhook type from each
// webhook's set of action types to show.
for (const [key, webhook] of Object.entries(allWebhooks)) {
const actionTypes = Object.keys(webhook)
const defaultAction = actionTypes ? actionTypes[0] : null
const defaultAction = actionTypes.length > 0 ? actionTypes[0] : ''
const initialWebhook = {
const initialWebhook: InitialWebhook = {
name: key,
actionTypes,
data: webhook[defaultAction],
data: defaultAction ? webhook[defaultAction] : {},
}
// remove all nested params for the initial webhooks page, we'll load
@@ -54,13 +66,16 @@ export async function getInitialPageWebhooks(version) {
// returns the webhook data for the given version and webhook category (e.g.
// `check_run`) -- this includes all the data per webhook action type and all
// nested parameters
export async function getWebhook(version, webhookCategory) {
export async function getWebhook(
version: string,
webhookCategory: string,
): Promise<Record<string, any> | undefined> {
const webhooks = await getWebhooks(version)
return webhooks[webhookCategory]
}
// returns all the webhook data for the given version
export async function getWebhooks(version) {
export async function getWebhooks(version: string): Promise<Record<string, any>> {
const openApiVersion = getOpenApiVersion(version)
if (!webhooksCache.has(openApiVersion)) {
// The `readCompressedJsonFileFallback()` function
@@ -73,5 +88,5 @@ export async function getWebhooks(version) {
)
}
return webhooksCache.get(openApiVersion)
return webhooksCache.get(openApiVersion) || Promise.resolve({})
}

View File

@@ -10,7 +10,7 @@ const router = express.Router()
// Example request:
//
// /api/webhooks/v1?category=check_run&version=free-pro-team%40latest
router.get('/v1', async function webhooks(req, res, next) {
router.get('/v1', async function webhooks(req, res) {
if (!req.query.category) {
return res.status(400).json({ error: "Missing 'category' in query string" })
}
@@ -27,7 +27,7 @@ router.get('/v1', async function webhooks(req, res, next) {
return res.status(404).json({ error: notFoundError })
}
const webhook = await getWebhook(webhookVersion, req.query.category)
const webhook = await getWebhook(webhookVersion, req.query.category as string)
if (webhook) {
if (process.env.NODE_ENV !== 'development') {