Merge remote-tracking branch 'private/main'
This commit is contained in:
1
.github/workflows/sync-openapi.yml
vendored
1
.github/workflows/sync-openapi.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
||||
# will be checked out
|
||||
repository: github/models-gateway
|
||||
path: models-gateway
|
||||
ref: main
|
||||
|
||||
- uses: ./.github/actions/node-npm-setup
|
||||
|
||||
|
||||
15
.github/workflows/triage-issue-comments.yml
vendored
15
.github/workflows/triage-issue-comments.yml
vendored
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}'.`)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
131
src/ghes-releases/scripts/create-enterprise-issue.js → src/ghes-releases/scripts/create-enterprise-issue.ts
Executable file → Normal file
131
src/ghes-releases/scripts/create-enterprise-issue.js → src/ghes-releases/scripts/create-enterprise-issue.ts
Executable file → Normal 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'),
|
||||
@@ -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"
|
||||
|
||||
53
src/ghes-releases/scripts/release-banner.js → src/ghes-releases/scripts/release-banner.ts
Executable file → Normal file
53
src/ghes-releases/scripts/release-banner.js → src/ghes-releases/scripts/release-banner.ts
Executable file → Normal 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)
|
||||
})
|
||||
21
src/ghes-releases/scripts/update-enterprise-dates.js → src/ghes-releases/scripts/update-enterprise-dates.ts
Executable file → Normal file
21
src/ghes-releases/scripts/update-enterprise-dates.js → src/ghes-releases/scripts/update-enterprise-dates.ts
Executable file → Normal 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,
|
||||
@@ -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={{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({})
|
||||
}
|
||||
@@ -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') {
|
||||
Reference in New Issue
Block a user