diff --git a/assets/images/help/dependabot/dependabot-vnet-active-jobs.png b/assets/images/help/dependabot/dependabot-vnet-active-jobs.png new file mode 100644 index 000000000000..4631c6415855 Binary files /dev/null and b/assets/images/help/dependabot/dependabot-vnet-active-jobs.png differ diff --git a/assets/images/help/dependabot/dependabot-vnet-logs.png b/assets/images/help/dependabot/dependabot-vnet-logs.png new file mode 100644 index 000000000000..3124b4c54245 Binary files /dev/null and b/assets/images/help/dependabot/dependabot-vnet-logs.png differ diff --git a/content/code-security/dependabot/working-with-dependabot/index.md b/content/code-security/dependabot/working-with-dependabot/index.md index 682f68f21b14..1d05b503b211 100644 --- a/content/code-security/dependabot/working-with-dependabot/index.md +++ b/content/code-security/dependabot/working-with-dependabot/index.md @@ -22,4 +22,5 @@ children: - /guidance-for-the-configuration-of-private-registries-for-dependabot - /dependabot-options-reference - /setting-dependabot-to-run-on-self-hosted-runners-using-arc + - /setting-dependabot-to-run-on-github-hosted-runners-using-vnet --- diff --git a/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-github-hosted-runners-using-vnet.md b/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-github-hosted-runners-using-vnet.md new file mode 100644 index 000000000000..d50105b5a162 --- /dev/null +++ b/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-github-hosted-runners-using-vnet.md @@ -0,0 +1,112 @@ +--- +title: Setting up Dependabot to run on github-hosted action runners using the Azure Private Network +intro: You can configure an Azure Virtual Network (VNET) to run {% data variables.product.prodname_dependabot %} on {% data variables.product.company_short %}-hosted runners. +versions: + feature: dependabot-vnet-support +permissions: '{% data reusables.permissions.dependabot-various-tasks %}' +topics: + - Repositories + - Dependabot + - Version updates + - Security updates + - Dependencies + - Pull requests +allowTitleToDifferFromFilename: true +shortTitle: Configure VNET +--- + +## Configuring VNET for {% data variables.product.prodname_dependabot_updates %} + +{% data reusables.dependabot.vnet-support-private-preview-note %} + +This article provides step-by-step instructions for running {% data variables.product.prodname_dependabot %} on {% data variables.product.company_short %}-hosted runners configured with VNET. The article explains: + +* How to create runner groups for your enterprise or organization with a VNET configuration. +* How to create {% data variables.product.company_short %}-hosted runners for {% data variables.product.prodname_dependabot %} in the runner group. +* How to enable {% data variables.product.prodname_dependabot %} on large runners. +* How to configure Azure VNET firewall IP rules. + +To use {% data variables.product.company_short %}-hosted runners with Azure VNET, you first need to configure your Azure resources, then create a private network configuration in {% data variables.product.github %}. + +## Configuring Azure resources + +To learn how to use {% data variables.product.company_short %}-hosted runners with an Azure private network, see [Configuring your Azure resources](/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/configuring-private-networking-for-github-hosted-runners-in-your-enterprise#configuring-your-azure-resources). + +> [!NOTE] +> +> * The `databaseId` which is required in the script for configuring the Azure resources can refer to any of the following depending on whether you are configuring the resources for an enterprise or an organization: +> * The enterprise slug, which you can identify by looking at the URL for your enterprise, `https://github.com/enterprises/SLUG`, or +> * The login for the organization account, which you can identify by looking at the URL for your organization, `https://github.com/organizations/ORGANIZATION_LOGIN`. +> * The script will return the full payload for the created resource. The `GitHubId` hash value returned in the payload for the created resource is the network settings resource ID you will use in the next steps while setting up a network configuration in {% data variables.product.github %} + +## Configuring a VNET-injected runner for {% data variables.product.prodname_dependabot_updates %} in your enterprise + +After configuring your Azure resources, you can use an Azure Virtual Network (VNET) for private networking by creating a network configuration{% ifversion ghec %} at the enterprise or organization level{% else %} at the organization level{% endif %}. Then, you can associate that network configuration to runner groups. + +1. Add a new network configuration for your enterprise. See [Add a new network configuration for your enterprise](/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/configuring-private-networking-for-github-hosted-runners-in-your-enterprise#1-add-a-new-network-configuration-for-your-enterprise) +1. Create a runner group for the enterprise and select the organizations that you want to run {% data variables.product.prodname_dependabot_updates %} for. See [Create a runner group for your enterprise](/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/configuring-private-networking-for-github-hosted-runners-in-your-enterprise#2-create-a-runner-group-for-your-enterprise) +1. Create and add a {% data variables.product.company_short %}-hosted runner to the enterprise runner group. See [Adding a larger runner to an enterprise](/actions/using-github-hosted-runners/using-larger-runners/managing-larger-runners#adding-a-larger-runner-to-an-enterprise). Important points are as follows: + * The runner name must be **dependabot** + * Choose a Linux x64 platform. + * Select the suitable Ubuntu version. + * When adding your {% data variables.product.company_short %}-hosted runner to a runner group, select the runner group you created in the previous step. + + > [!NOTE] + > Naming the {% data variables.product.company_short %}-hosted runner **dependabot** assigns the **dependabot** label to the runner, which enables it to pick up jobs triggered by {% data variables.product.prodname_dependabot %} on actions. + +{% ifversion fpt or ghec %} + + + +## Enabling {% data variables.product.prodname_dependabot %} for the organization + +You now need to enable {% data variables.product.prodname_dependabot %} on _self-hosted runners_ for your organization in order to enable {% data variables.product.prodname_dependabot %} on large runners. See [Enabling or disabling {% data variables.product.prodname_dependabot %} on larger runners](/code-security/dependabot/working-with-dependabot/about-dependabot-on-github-actions-runners#enabling-or-disabling-dependabot-on-larger-runners). + +{% data reusables.profile.access_org %} +{% data reusables.organizations.org_settings %} +1. In the "Security" section of the sidebar, select the **{% data variables.product.UI_advanced_security %}** dropdown menu, then click **{% data variables.product.prodname_global_settings_caps %}**. +1. Under **{% data variables.product.prodname_dependabot %}**, select **{% data variables.product.prodname_dependabot %} on self-hosted runners**. This step is required, as it ensures that future {% data variables.product.prodname_dependabot %} jobs will run on the larger {% data variables.product.company_short %}-hosted runner that has the `dependabot` name. + +{% endif %} + +## Triggering a {% data variables.product.prodname_dependabot %} run + +Now that you've set up private networking with VNET, you can start a {% data variables.product.prodname_dependabot %} run. + +{% data reusables.dependabot.trigger-run %} + +## Checking logs and active jobs for {% data variables.product.prodname_dependabot_updates %} + +* You can view the logs of the {% data variables.product.prodname_dependabot %} workflow in the **Actions** tab of your repository. Ensure you select the {% data variables.product.prodname_dependabot %} job on the left sidebar of the Actions page. + + ![Example of log for a "Dependabot in vnet" workflow. The Dependabot job is highlighted with an orange outline. ](/assets/images/help/dependabot/dependabot-vnet-logs.png) + +* You can view the active jobs in the page containing informatuon about the runner. To access that page, click the **Policies** tab for the enterprise, select **Actions** on the left sidebar, click the **Runner group** tab, and select your runner. + + ![Screenshot showing a Dependabot runner's active jobs.](/assets/images/help/dependabot/dependabot-vnet-active-jobs.png) + +## Configuring Azure VNET firewall IP rules + +If your Azure VNET environment is configured with a firewall with an IP allowlist, you may need to update your list of allowed IP addresses to use the {% data variables.product.company_short %}-hosted runners IP addresses sourced from the meta API endpoint. + +* {% data variables.product.github %} provides the following public endpoint for its IP ranges: + > GET +* Copy and paste the following curl command in your terminal or command prompt and replace the placeholder bearer token value with your actual value. + + ```bash copy + curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer YOUR-TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/meta + ``` + +* From the response, look for the **actions** key. + + ```bash + "actions": [ ... ] + ``` + + These are the IP ranges used by {% data variables.product.prodname_actions %} runners, including {% data variables.product.prodname_dependabot %} and hosted runners. + +* Add these IPs to your firewall allowlist. diff --git a/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc.md b/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc.md index 73dd535bf426..eb1a61aa67a9 100644 --- a/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc.md +++ b/content/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc.md @@ -178,17 +178,11 @@ Don't forget to add the following setting to the runner scale set configuration Now that you've set up ARC, you can start a {% data variables.product.prodname_dependabot %} run. -{% data reusables.repositories.navigate-to-repo %} -{% data reusables.repositories.navigate-to-insights %} -{% data reusables.repositories.click-dependency-graph %} - -1. Under "Dependency graph", click **{% data variables.product.prodname_dependabot %}**. -1. To the right of the name of manifest file you're interested in, click **Recent update jobs**. -1. If there are no recent update jobs for the manifest file, click **Check for updates** to re-run a {% data variables.product.prodname_dependabot %} version updates'job and check for new updates to dependencies for that ecosystem. +{% data reusables.dependabot.trigger-run %} ## Viewing the generated ARC runners -You can the ARC runners that have been created for the {% data variables.product.prodname_dependabot %} job. +You can view the ARC runners that have been created for the {% data variables.product.prodname_dependabot %} job. {% data reusables.repositories.navigate-to-repo %} {% data reusables.repositories.actions-tab %} diff --git a/data/features/dependabot-vnet-support.yml b/data/features/dependabot-vnet-support.yml new file mode 100644 index 000000000000..fb9af553a7d4 --- /dev/null +++ b/data/features/dependabot-vnet-support.yml @@ -0,0 +1,7 @@ +# References: +# Issue #18165 - Dependabot adds vNet (Virtual Network) support for security and version updates + +versions: + fpt: '*' + ghec: '*' + ghes: '>3.17' diff --git a/data/reusables/dependabot/trigger-run.md b/data/reusables/dependabot/trigger-run.md new file mode 100644 index 000000000000..1ecb56e3c391 --- /dev/null +++ b/data/reusables/dependabot/trigger-run.md @@ -0,0 +1,7 @@ +{% data reusables.repositories.navigate-to-repo %} +{% data reusables.repositories.navigate-to-insights %} +{% data reusables.repositories.click-dependency-graph %} + +1. Under "Dependency graph", click **{% data variables.product.prodname_dependabot %}**. +1. To the right of the name of manifest file you're interested in, click **Recent update jobs**. +1. If there are no recent update jobs for the manifest file, click **Check for updates** to re-run a {% data variables.product.prodname_dependabot %} version updates'job and check for new updates to dependencies for that ecosystem. diff --git a/data/reusables/dependabot/vnet-arc-note.md b/data/reusables/dependabot/vnet-arc-note.md index 2a56d5f812e3..d08e19885220 100644 --- a/data/reusables/dependabot/vnet-arc-note.md +++ b/data/reusables/dependabot/vnet-arc-note.md @@ -1,6 +1,7 @@ -{% ifversion dependabot-arc-support %} +{% ifversion dependabot-vnet-support or dependabot-arc-support %} -> [!WARNING] Private networking is currently unsupported with an Azure Virtual Network (VNET) for {% data variables.product.prodname_dependabot %} on {% data variables.product.prodname_actions %}. By using VNET, you do so at your own risk, and {% data variables.product.github %} cannot currently support you if problems arise. Private networking is supported for the {% data variables.product.prodname_actions_runner_controller %}. See [AUTOTITLE](/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc). +>[!NOTE] +> Private networking is supported with either an Azure Virtual Network (VNET) or the Actions Runner Controller (ARC) for {% data variables.product.prodname_dependabot %} on {% data variables.product.prodname_actions %}. See [AUTOTITLE](/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-self-hosted-runners-using-arc) and [AUTOTITLE](/code-security/dependabot/working-with-dependabot/setting-dependabot-to-run-on-github-hosted-runners-using-vnet) for more information, and instruction. {% else %} diff --git a/data/reusables/dependabot/vnet-support-private-preview-note.md b/data/reusables/dependabot/vnet-support-private-preview-note.md new file mode 100644 index 000000000000..86b409be5fc0 --- /dev/null +++ b/data/reusables/dependabot/vnet-support-private-preview-note.md @@ -0,0 +1,2 @@ +> [!NOTE] +> VNET support for {% data variables.product.prodname_dependabot %} on {% data variables.product.prodname_actions %} is currently in {% data variables.release-phases.public_preview %} and subject to change. diff --git a/package-lock.json b/package-lock.json index d0e3cd903a96..a974a87c4e20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,7 +110,7 @@ "@playwright/test": "^1.50", "@types/accept-language-parser": "1.5.7", "@types/cheerio": "^0.22.35", - "@types/connect-timeout": "0.0.39", + "@types/connect-timeout": "1.9.0", "@types/cookie": "0.6.0", "@types/cookie-parser": "1.4.8", "@types/event-to-promise": "^0.7.5", @@ -4292,9 +4292,9 @@ } }, "node_modules/@types/connect-timeout": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/connect-timeout/-/connect-timeout-0.0.39.tgz", - "integrity": "sha512-PHUX9yixjm4qjQ27Uz+F5cyG9Fio9iY7e6eWHvNT0wbA8eu9JaDqxRRiXn5uYnd09zx1fM+wEHnuBHMm9gwOuQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/connect-timeout/-/connect-timeout-1.9.0.tgz", + "integrity": "sha512-tKAbro0/ATFeaSVa/N3Gv981+7KbuNQjoZEW8uI4NEdyxquWnylC5UTXwIOc2HD2q22ZjLr8H5YVKZL2+pGBGw==", "dev": true, "dependencies": { "@types/express": "*" diff --git a/package.json b/package.json index 50f7182ca8fc..89c7f715815d 100644 --- a/package.json +++ b/package.json @@ -344,7 +344,7 @@ "@playwright/test": "^1.50", "@types/accept-language-parser": "1.5.7", "@types/cheerio": "^0.22.35", - "@types/connect-timeout": "0.0.39", + "@types/connect-timeout": "1.9.0", "@types/cookie": "0.6.0", "@types/cookie-parser": "1.4.8", "@types/event-to-promise": "^0.7.5", diff --git a/src/article-api/tests/pageinfo.js b/src/article-api/tests/pageinfo.ts similarity index 83% rename from src/article-api/tests/pageinfo.js rename to src/article-api/tests/pageinfo.ts index a412da2efaa3..38ac968c388a 100644 --- a/src/article-api/tests/pageinfo.js +++ b/src/article-api/tests/pageinfo.ts @@ -4,7 +4,18 @@ import { get } from '#src/tests/helpers/e2etest.js' import { SURROGATE_ENUMS } from '#src/frame/middleware/set-fastly-surrogate-key.js' import { latest } from '#src/versions/lib/enterprise-server-releases.js' -const makeURL = (pathname) => `/api/article/meta?${new URLSearchParams({ pathname })}` +const makeURL = (pathname: string): string => + `/api/article/meta?${new URLSearchParams({ pathname })}` + +interface PageMetadata { + product: string + title: string + intro: string +} + +interface ErrorResponse { + error: string +} describe('pageinfo api', () => { beforeAll(() => { @@ -27,7 +38,7 @@ describe('pageinfo api', () => { test('happy path', async () => { const res = await get(makeURL('/en/get-started/start-your-journey')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.product).toBe('Get started') expect(meta.title).toBe('Start your journey') expect(meta.intro).toBe( @@ -45,28 +56,28 @@ describe('pageinfo api', () => { test('a pathname that does not exist', async () => { const res = await get(makeURL('/en/never/heard/of')) expect(res.statusCode).toBe(404) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("No page found for '/en/never/heard/of'") }) test("no 'pathname' query string at all", async () => { const res = await get('/api/article/meta') expect(res.statusCode).toBe(400) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("No 'pathname' query") }) test("empty 'pathname' query string", async () => { const res = await get('/api/article/meta?pathname=%20') expect(res.statusCode).toBe(400) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("'pathname' query empty") }) test('repeated pathname query string key', async () => { const res = await get('/api/article/meta?pathname=a&pathname=b') expect(res.statusCode).toBe(400) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("Multiple 'pathname' keys") }) @@ -75,28 +86,28 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/olden-days')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toBe('HubGit.com Fixture Documentation') } // Trailing slashes are always removed { const res = await get(makeURL('/en/olden-days/')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toBe('HubGit.com Fixture Documentation') } // Short code for latest version { const res = await get(makeURL('/en/enterprise-server@latest/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.intro).toMatch(/\(not on fpt\)/) } // A URL that doesn't have fpt as an available version { const res = await get(makeURL('/en/get-started/versioning/only-ghec-and-ghes')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toBe('Only in Enterprise Cloud and Enterprise Server') } }) @@ -108,14 +119,14 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.intro).toMatch(/\(on fpt\)/) } // Second on any other version { const res = await get(makeURL('/en/enterprise-server@latest/get-started/liquid/ifversion')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.intro).toMatch(/\(not on fpt\)/) } }) @@ -125,7 +136,7 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('HubGit.com Fixture Documentation') } // enterprise-server with language specified @@ -137,7 +148,7 @@ describe('pageinfo api', () => { { const res = await get(makeURL(`/en/enterprise-server@${latest}`)) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('HubGit Enterprise Server Fixture Documentation') } }) @@ -147,14 +158,14 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('HubGit.com Fixture Documentation') } // enterprise-server without language specified { const res = await get(makeURL('/enterprise-server@latest')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('HubGit Enterprise Server Fixture Documentation') } }) @@ -169,7 +180,7 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/enterprise-server@3.2')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('GitHub Enterprise Server 3.2 Help Documentation') } @@ -177,7 +188,7 @@ describe('pageinfo api', () => { { const res = await get(makeURL('/en/enterprise/11.10.340')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.title).toMatch('GitHub Enterprise Server 11.10.340 Help Documentation') } }) @@ -185,14 +196,14 @@ describe('pageinfo api', () => { test('pathname has to start with /', async () => { const res = await get(makeURL('ip')) expect(res.statusCode).toBe(400) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("'pathname' has to start with /") }) test("pathname can't contain spaces /", async () => { const res = await get(makeURL('/en foo bar')) expect(res.statusCode).toBe(400) - const { error } = JSON.parse(res.body) + const { error } = JSON.parse(res.body) as ErrorResponse expect(error).toBe("'pathname' cannot contain whitespace") }) @@ -200,7 +211,7 @@ describe('pageinfo api', () => { test('Japanese page', async () => { const res = await get(makeURL('/ja/get-started/start-your-journey/hello-world')) expect(res.statusCode).toBe(200) - const meta = JSON.parse(res.body) + const meta = JSON.parse(res.body) as PageMetadata expect(meta.product).toBe('はじめに') expect(meta.title).toBe('こんにちは World') expect(meta.intro).toBe('この Hello World 演習に従って、HubGit の使用を開始します。') @@ -213,8 +224,8 @@ describe('pageinfo api', () => { // even exist on disk. So it'll fall back to English. const translationRes = await get(makeURL('/ja/get-started/start-your-journey')) expect(translationRes.statusCode).toBe(200) - const en = JSON.parse(enRes.body) - const translation = JSON.parse(translationRes.body) + const en = JSON.parse(enRes.body) as PageMetadata + const translation = JSON.parse(translationRes.body) as PageMetadata expect(en.title).toBe(translation.title) expect(en.intro).toBe(translation.intro) }) diff --git a/src/assets/lib/image-density.js b/src/assets/lib/image-density.ts similarity index 63% rename from src/assets/lib/image-density.js rename to src/assets/lib/image-density.ts index 0ca465a3a00a..1bbcc97f9474 100644 --- a/src/assets/lib/image-density.js +++ b/src/assets/lib/image-density.ts @@ -1,8 +1,12 @@ import fs from 'fs' +interface ImageDensityMap { + [path: string]: string +} + const file = fs.readFileSync('./src/assets/lib/image-density.txt', 'utf8') -export const IMAGE_DENSITY = Object.fromEntries( +export const IMAGE_DENSITY: ImageDensityMap = Object.fromEntries( file.split('\n').map((line) => { const [path, density] = line.split(' ') return [path, density] diff --git a/src/color-schemes/tests/use-theme.js b/src/color-schemes/tests/use-theme.ts similarity index 82% rename from src/color-schemes/tests/use-theme.js rename to src/color-schemes/tests/use-theme.ts index 307da9fe4d27..9d2423418531 100644 --- a/src/color-schemes/tests/use-theme.js +++ b/src/color-schemes/tests/use-theme.ts @@ -5,11 +5,23 @@ import { getCssTheme, defaultCSSTheme, defaultComponentTheme, -} from '../components/useTheme.ts' +} from '../components/useTheme' + +interface ThemeCookieValue { + color_mode?: string + light_theme?: { + name: string + color_mode: string + } + dark_theme?: { + name: string + color_mode: string + } +} describe('getTheme basics', () => { test('always return an object with certain keys', () => { - const cookieValue = JSON.stringify({}) + const cookieValue = JSON.stringify({} as ThemeCookieValue) expect(getCssTheme(cookieValue)).toEqual(defaultCSSTheme) expect(getComponentTheme(cookieValue)).toEqual(defaultComponentTheme) }) @@ -25,7 +37,7 @@ describe('getTheme basics', () => { color_mode: 'dark', light_theme: { name: 'light_colorblind', color_mode: 'light' }, dark_theme: { name: 'dark_tritanopia', color_mode: 'dark' }, - }) + } as ThemeCookieValue) const cssTheme = getCssTheme(cookieValue) expect(cssTheme.colorMode).toBe('dark') diff --git a/src/content-render/liquid/tool.js b/src/content-render/liquid/tool.js index 7a7b47dd8965..8851fb07e5ea 100644 --- a/src/content-render/liquid/tool.js +++ b/src/content-render/liquid/tool.js @@ -1,5 +1,5 @@ -import { allTools } from '#src/tools/lib/all-tools.js' -import { allPlatforms } from '#src/tools/lib/all-platforms.js' +import { allTools } from '#src/tools/lib/all-tools.ts' +import { allPlatforms } from '#src/tools/lib/all-platforms.ts' export const tags = Object.keys(allTools).concat(allPlatforms).concat(['rowheaders']) diff --git a/src/content-render/tests/data.js b/src/content-render/tests/data.js index ac24d0a59e79..61922e69526c 100644 --- a/src/content-render/tests/data.js +++ b/src/content-render/tests/data.js @@ -17,6 +17,9 @@ describe('data tag', () => { foo: 'Foo', }, }, + ui: { + alerts: {}, + }, }, }) languages.en.dir = dd.root diff --git a/src/content-render/unified/alerts.js b/src/content-render/unified/alerts.js index 7f2a74368051..d4feac161b8e 100644 --- a/src/content-render/unified/alerts.js +++ b/src/content-render/unified/alerts.js @@ -7,11 +7,11 @@ import { h } from 'hastscript' import octicons from '@primer/octicons' const alertTypes = { - NOTE: { icon: 'info', color: 'accent', title: 'Note' }, - IMPORTANT: { icon: 'report', color: 'done', title: 'Important' }, - WARNING: { icon: 'alert', color: 'attention', title: 'Warning' }, - TIP: { icon: 'light-bulb', color: 'success', title: 'Tip' }, - CAUTION: { icon: 'stop', color: 'danger', title: 'Caution' }, + NOTE: { icon: 'info', color: 'accent' }, + IMPORTANT: { icon: 'report', color: 'done' }, + WARNING: { icon: 'alert', color: 'attention' }, + TIP: { icon: 'light-bulb', color: 'success' }, + CAUTION: { icon: 'stop', color: 'danger' }, } // Must contain one of [!NOTE], [!IMPORTANT], ... @@ -22,7 +22,7 @@ const matcher = (node) => node.tagName === 'blockquote' && ALERT_REGEXP.test(JSON.stringify(node.children)) -export default function alerts() { +export default function alerts({ alertTitles = {} }) { return (tree) => { visit(tree, matcher, (node) => { const key = getAlertKey(node) @@ -35,7 +35,12 @@ export default function alerts() { node.tagName = 'div' node.properties.className = 'ghd-alert ghd-alert-' + alertType.color node.children = [ - h('p', { className: 'ghd-alert-title' }, getOcticonSVG(alertType.icon), alertType.title), + h( + 'p', + { className: 'ghd-alert-title' }, + getOcticonSVG(alertType.icon), + alertTitles[key] || '', + ), ...removeAlertSyntax(node.children), ] }) diff --git a/src/content-render/unified/processor.js b/src/content-render/unified/processor.js index 7b54abef213f..0f0c565270d1 100644 --- a/src/content-render/unified/processor.js +++ b/src/content-render/unified/processor.js @@ -73,7 +73,7 @@ export function createProcessor(context) { .use(rewriteForRowheaders) .use(rewriteImgSources) .use(rewriteAssetImgTags) - .use(alerts) + .use(alerts, context) // HTML AST above ^^^ .use(html) // String below vvv diff --git a/src/content-render/unified/rewrite-asset-img-tags.js b/src/content-render/unified/rewrite-asset-img-tags.js index 15fede7b3c87..23fd0fed9149 100644 --- a/src/content-render/unified/rewrite-asset-img-tags.js +++ b/src/content-render/unified/rewrite-asset-img-tags.js @@ -1,5 +1,5 @@ import { visit, SKIP } from 'unist-util-visit' -import { IMAGE_DENSITY } from '../../assets/lib/image-density.js' +import { IMAGE_DENSITY } from '../../assets/lib/image-density.ts' // This number must match a width we're willing to accept in a dynamic // asset URL. diff --git a/src/events/components/experiments/experiments.ts b/src/events/components/experiments/experiments.ts index 62157188fdee..6bd8f1ba291d 100644 --- a/src/events/components/experiments/experiments.ts +++ b/src/events/components/experiments/experiments.ts @@ -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: 10, // 10% of users will get the experiment + percentOfUsersToGetExperiment: 20, // 20% 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) diff --git a/src/events/lib/get-document-type.js b/src/events/lib/get-document-type.ts similarity index 52% rename from src/events/lib/get-document-type.js rename to src/events/lib/get-document-type.ts index 8313353d0c2f..1c04ae367c5b 100644 --- a/src/events/lib/get-document-type.js +++ b/src/events/lib/get-document-type.ts @@ -1,7 +1,14 @@ -// This function derives the document type from the *relative path* segment length, -// where a relative path refers to the content path starting with the product dir. -// For example: actions/index.md or github/getting-started-with-github/quickstart.md. -export default function getDocumentType(relativePath) { +/** + * Document types used by the system + */ +type DocumentType = 'homepage' | 'product' | 'category' | 'mapTopic' | 'article' | 'early-access' + +/** + * This function derives the document type from the *relative path* segment length, + * where a relative path refers to the content path starting with the product dir. + * For example: actions/index.md or github/getting-started-with-github/quickstart.md. + */ +export default function getDocumentType(relativePath: string): DocumentType { // A non-index file is ALWAYS considered an article in this approach, // even if it's at the category level (like actions/quickstart.md) if (!relativePath.endsWith('index.md')) { @@ -13,9 +20,15 @@ export default function getDocumentType(relativePath) { // Early Access has an extra tree segment, so it has a different number of segments. const isEarlyAccess = relativePath.startsWith('early-access') - const publicDocs = ['homepage', 'product', 'category', 'mapTopic'] + const publicDocs: DocumentType[] = ['homepage', 'product', 'category', 'mapTopic'] - const earlyAccessDocs = ['homepage', 'early-access', 'product', 'category', 'mapTopic'] + const earlyAccessDocs: DocumentType[] = [ + 'homepage', + 'early-access', + 'product', + 'category', + 'mapTopic', + ] // Anything beyond the largest depth is assumed to be a mapTopic return isEarlyAccess diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts index deaa0790fd0e..daeb38642e8c 100644 --- a/src/events/lib/schema.ts +++ b/src/events/lib/schema.ts @@ -1,6 +1,6 @@ import { languageKeys } from '#src/languages/lib/languages.js' import { allVersionKeys } from '#src/versions/lib/all-versions.js' -import { productIds } from '#src/products/lib/all-products.js' +import { productIds } from '#src/products/lib/all-products.ts' import { allTools } from 'src/tools/lib/all-tools.js' const versionPattern = '^\\d+(\\.\\d+)?(\\.\\d+)?$' // eslint-disable-line diff --git a/src/frame/lib/frontmatter.js b/src/frame/lib/frontmatter.js index 5e7c855ca3c7..d3942a44cf91 100644 --- a/src/frame/lib/frontmatter.js +++ b/src/frame/lib/frontmatter.js @@ -1,6 +1,6 @@ import parse from './read-frontmatter.js' import { allVersions } from '#src/versions/lib/all-versions.js' -import { allTools } from '#src/tools/lib/all-tools.js' +import { allTools } from '#src/tools/lib/all-tools.ts' import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js' const layoutNames = [ diff --git a/src/frame/lib/get-toc-items.js b/src/frame/lib/get-toc-items.js index f522f566e7c5..78a04ba53d6e 100644 --- a/src/frame/lib/get-toc-items.js +++ b/src/frame/lib/get-toc-items.js @@ -1,4 +1,4 @@ -import { productMap } from '#src/products/lib/all-products.js' +import { productMap } from '#src/products/lib/all-products.ts' const productTOCs = Object.values(productMap) .filter((product) => !product.external) .map((product) => product.toc.replace('content/', '')) diff --git a/src/frame/lib/page.js b/src/frame/lib/page.js index 0658db76f22f..06ef08ef71a5 100644 --- a/src/frame/lib/page.js +++ b/src/frame/lib/page.js @@ -4,19 +4,20 @@ import cheerio from 'cheerio' import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js' import generateRedirectsForPermalinks from '#src/redirects/lib/permalinks.js' import getEnglishHeadings from '#src/languages/lib/get-english-headings.js' +import { getAlertTitles } from '#src/languages/lib/get-alert-titles.ts' import getTocItems from './get-toc-items.js' import Permalink from './permalink.js' import { renderContent } from '#src/content-render/index.js' import processLearningTracks from '#src/learning-track/lib/process-learning-tracks.js' -import { productMap } from '#src/products/lib/all-products.js' +import { productMap } from '#src/products/lib/all-products.ts' import slash from 'slash' import readFileContents from './read-file-contents.js' import getLinkData from '#src/learning-track/lib/get-link-data.js' -import getDocumentType from '#src/events/lib/get-document-type.js' -import { allTools } from '#src/tools/lib/all-tools.js' +import getDocumentType from '#src/events/lib/get-document-type.ts' +import { allTools } from '#src/tools/lib/all-tools.ts' import { renderContentWithFallback } from '#src/languages/lib/render-with-fallback.js' import { deprecated, supported } from '#src/versions/lib/enterprise-server-releases.js' -import { allPlatforms } from '#src/tools/lib/all-platforms.js' +import { allPlatforms } from '#src/tools/lib/all-platforms.ts' // We're going to check a lot of pages' "ID" (the first part of // the relativePath) against `productMap` to make sure it's valid. @@ -200,6 +201,9 @@ class Page { context.englishHeadings = englishHeadings } + // pull translations for alerts + context.alertTitles = await getAlertTitles(this) + this.intro = await renderContentWithFallback(this, 'rawIntro', context) this.introPlainText = await renderContentWithFallback(this, 'rawIntro', context, { textOnly: true, diff --git a/src/frame/lib/path-utils.js b/src/frame/lib/path-utils.js index 9bf528040fc1..73c8898fc7ee 100644 --- a/src/frame/lib/path-utils.js +++ b/src/frame/lib/path-utils.js @@ -2,7 +2,7 @@ import slash from 'slash' import path from 'path' import patterns from './patterns.js' import { latest } from '#src/versions/lib/enterprise-server-releases.js' -import { productIds } from '#src/products/lib/all-products.js' +import { productIds } from '#src/products/lib/all-products.ts' import { allVersions } from '#src/versions/lib/all-versions.js' import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js' const supportedVersions = new Set(Object.keys(allVersions)) diff --git a/src/landings/tests/curated-homepage-links.js b/src/landings/tests/curated-homepage-links.ts similarity index 88% rename from src/landings/tests/curated-homepage-links.js rename to src/landings/tests/curated-homepage-links.ts index 5b1374d3afcc..3133167320c3 100644 --- a/src/landings/tests/curated-homepage-links.js +++ b/src/landings/tests/curated-homepage-links.ts @@ -1,4 +1,5 @@ import { describe, expect, test, vi } from 'vitest' +import cheerio from 'cheerio' import { getDOM } from '#src/tests/helpers/e2etest.js' @@ -11,8 +12,8 @@ describe('curated homepage links', () => { expect($links.length).toBeGreaterThanOrEqual(6) // Check that each link is localized and includes a title and intro - $links.each((i, el) => { - const linkUrl = $(el).attr('href') + $links.each((i: number, el: cheerio.Element) => { + const linkUrl = $(el).attr('href') as string expect(linkUrl.startsWith('/en/')).toBe(true) expect( diff --git a/src/landings/tests/homepage.js b/src/landings/tests/homepage.ts similarity index 100% rename from src/landings/tests/homepage.js rename to src/landings/tests/homepage.ts diff --git a/src/languages/lib/get-alert-titles.ts b/src/languages/lib/get-alert-titles.ts new file mode 100644 index 000000000000..52406abb62f4 --- /dev/null +++ b/src/languages/lib/get-alert-titles.ts @@ -0,0 +1,31 @@ +import fs from 'fs/promises' +import path from 'path' +import yaml from 'js-yaml' +import languages from './languages' + +const cache: Record = {} + +export async function getAlertTitles(page: Record) { + const { languageCode } = page + if (cache[languageCode]) return cache[languageCode] + + let file = '' + let yamlFile: Record = {} + if (languageCode !== 'en') { + try { + const { dir } = languages[languageCode] + file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8') + yamlFile = yaml.load(file) as Record + } catch (e) { + console.warn(`Failed to load translated alert titles`, e) + } + } + if (!file || !yamlFile.alerts) { + const { dir } = languages.en + file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8') + yamlFile = yaml.load(file) as Record + } + + cache[languageCode] = yamlFile.alerts + return cache[languageCode] +} diff --git a/src/products/lib/all-products.js b/src/products/lib/all-products.js deleted file mode 100644 index 1caa7a7612b8..000000000000 --- a/src/products/lib/all-products.js +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' -import frontmatter from '#src/frame/lib/read-frontmatter.js' -import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js' -import removeFPTFromPath from '#src/versions/lib/remove-fpt-from-path.js' -import { ROOT } from '#src/frame/lib/constants.js' - -// Both internal and external products are specified in content/index.md -const homepage = path.posix.join(ROOT, 'content/index.md') -export const { data } = frontmatter(await fs.readFile(homepage, 'utf8')) - -export const productIds = data.children - -const externalProducts = data.externalProducts -const internalProducts = {} - -for (const productId of productIds) { - const relPath = productId - const dir = path.join(ROOT, 'content', relPath) - - // Early Access may not exist in the current checkout - try { - await fs.readdir(dir) - } catch (e) { - continue - } - - const toc = path.posix.join(dir, 'index.md') - const { data } = frontmatter(await fs.readFile(toc, 'utf8')) - const applicableVersions = getApplicableVersions(data.versions, toc) - const href = removeFPTFromPath(path.posix.join('/', applicableVersions[0], productId)) - - // Note that a special middleware called `render-product-map.js` later - // mutates this object by adding a `nameRendered` property to each product. - // It's the outcome of rendering out possible Liquid from the - // `shortTitle` or `title` after all the other contextualizers have run. - internalProducts[productId] = { - id: productId, - name: data.shortTitle || data.title, - href, - dir, - toc, - wip: data.wip || false, - hidden: data.hidden || false, - versions: applicableVersions, - } -} - -export const productMap = Object.assign({}, internalProducts, externalProducts) diff --git a/src/products/lib/all-products.ts b/src/products/lib/all-products.ts new file mode 100644 index 000000000000..f858812431cc --- /dev/null +++ b/src/products/lib/all-products.ts @@ -0,0 +1,85 @@ +import fs from 'fs/promises' +import path from 'path' +import frontmatter from '#src/frame/lib/read-frontmatter.js' +import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js' +import removeFPTFromPath from '#src/versions/lib/remove-fpt-from-path.js' +import { ROOT } from '#src/frame/lib/constants.js' + +/** + * Represents a product in the documentation + */ +export interface Product { + /** Unique identifier for the product */ + id: string + /** Display name of the product (short title or title) */ + name: string + /** URL path to the product's landing page */ + href: string + /** Directory path to the product's content */ + dir: string + /** Path to the product's table of contents file */ + toc: string + /** Whether the product is a work in progress */ + wip: boolean + /** Whether the product is hidden from the UI */ + hidden: boolean + /** Applicable versions for the product */ + versions: string[] + /** Rendered name (added later by middleware) */ + nameRendered?: string + /** Whether the product is external */ + external?: boolean +} + +/** + * Map of product IDs to their corresponding product data + */ +export interface ProductMap { + [productId: string]: Product +} + +// Both internal and external products are specified in content/index.md +const homepage = path.posix.join(ROOT, 'content/index.md') +export const { data } = frontmatter(await fs.readFile(homepage, 'utf8')) + +export const productIds: string[] = data?.children || [] + +const externalProducts = (data?.externalProducts || {}) as ProductMap +const internalProducts: ProductMap = {} + +for (const productId of productIds) { + const relPath = productId + const dir = path.join(ROOT, 'content', relPath) + + // Early Access may not exist in the current checkout + try { + await fs.readdir(dir) + } catch { + continue + } + + const toc = path.posix.join(dir, 'index.md') + const fileContent = await fs.readFile(toc, 'utf8') + const { data: tocData } = frontmatter(fileContent) + if (tocData) { + const applicableVersions = getApplicableVersions(tocData.versions, toc) + const href = removeFPTFromPath(path.posix.join('/', applicableVersions[0], productId)) + + // Note that a special middleware called `render-product-map.js` later + // mutates this object by adding a `nameRendered` property to each product. + // It's the outcome of rendering out possible Liquid from the + // `shortTitle` or `title` after all the other contextualizers have run. + internalProducts[productId] = { + id: productId, + name: tocData.shortTitle || tocData.title || productId, + href, + dir, + toc, + wip: tocData.wip || false, + hidden: tocData.hidden || false, + versions: applicableVersions, + } + } +} + +export const productMap: ProductMap = Object.assign({}, internalProducts, externalProducts) diff --git a/src/products/lib/get-product-groups.ts b/src/products/lib/get-product-groups.ts index b651ccdc8879..e7b785ec74c3 100644 --- a/src/products/lib/get-product-groups.ts +++ b/src/products/lib/get-product-groups.ts @@ -86,20 +86,30 @@ async function getPage( } } +interface ProductGroupData { + name: string + icon?: string + octicon?: string + children: string[] +} + export async function getProductGroups( pageMap: PageMap, lang: string, context: Context, ): Promise { + // Handle case where data or childGroups might be undefined + const childGroups = data?.childGroups || [] + return await Promise.all( - data.childGroups!.map(async (group) => { + childGroups.map(async (group: ProductGroupData) => { return { name: group.name, icon: group.icon || null, octicon: group.octicon || null, // Typically the children are product IDs, but we support deeper page paths too children: ( - await Promise.all(group.children.map((id) => getPage(id, lang, pageMap, context))) + await Promise.all(group.children.map((id: string) => getPage(id, lang, pageMap, context))) ).filter(Boolean) as ProductGroupChild[], } }), diff --git a/src/products/tests/products.js b/src/products/tests/products.ts similarity index 78% rename from src/products/tests/products.js rename to src/products/tests/products.ts index 85bc9d80cc2e..a827ba617bbf 100644 --- a/src/products/tests/products.js +++ b/src/products/tests/products.ts @@ -1,8 +1,9 @@ import { describe, expect, test } from 'vitest' import { getJsonValidator } from '#src/tests/lib/validate-json-schema.js' -import { productMap } from '#src/products/lib/all-products.js' +import { productMap } from '#src/products/lib/all-products.ts' import { formatAjvErrors } from '#src/tests/helpers/schemas.js' +// @ts-ignore - Products schema doesn't have TypeScript types yet import schema from '#src/tests/helpers/schemas/products-schema.js' const validate = getJsonValidator(schema) @@ -16,9 +17,9 @@ describe('products module', () => { test('every product is valid', () => { Object.values(productMap).forEach((product) => { const isValid = validate(product) - let errors + let errors: string | undefined - if (!isValid) { + if (!isValid && validate.errors) { errors = formatAjvErrors(validate.errors) } expect(isValid, errors).toBe(true) diff --git a/src/release-notes/tests/yaml.js b/src/release-notes/tests/yaml.ts similarity index 77% rename from src/release-notes/tests/yaml.js rename to src/release-notes/tests/yaml.ts index 7931588122fe..0246ca11a93a 100644 --- a/src/release-notes/tests/yaml.js +++ b/src/release-notes/tests/yaml.ts @@ -7,6 +7,14 @@ import yaml from 'js-yaml' import { liquid } from '#src/content-render/index.js' +interface ReleaseNoteContent { + intro: string + sections: { + [key: string]: Array + } + currentWeek?: boolean +} + const ghesReleaseNoteRootPath = 'data/release-notes' const yamlWalkOptions = { globs: ['**/*.yml'], @@ -18,17 +26,17 @@ const yamlFileList = walk(ghesReleaseNoteRootPath, yamlWalkOptions).sort() describe('lint enterprise release notes', () => { if (yamlFileList.length < 1) return describe.each(yamlFileList)('%s', (yamlAbsPath) => { - let yamlContent + let yamlContent: ReleaseNoteContent const relativePath = path.relative('', yamlAbsPath) beforeAll(async () => { const fileContents = await readFile(yamlAbsPath, 'utf8') - yamlContent = yaml.load(fileContents) + yamlContent = yaml.load(fileContents) as ReleaseNoteContent }) test('contains valid liquid', () => { const { intro, sections } = yamlContent - let toLint = { intro } + let toLint: Record = { intro } for (const key in sections) { const section = sections[key] const label = `sections.${key}` @@ -37,7 +45,10 @@ describe('lint enterprise release notes', () => { toLint = { ...toLint, ...{ [label]: section.join('\n') } } } else { for (const prop in section) { - toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } + const value = section[prop] + if (typeof value === 'string') { + toLint = { ...toLint, ...{ [`${label}.${prop}`]: value } } + } } } }) @@ -49,7 +60,7 @@ describe('lint enterprise release notes', () => { } }) - const currentWeeksFound = [] + const currentWeeksFound: string[] = [] test('does not have more than one yaml file with currentWeek set to true', () => { if (!yamlAbsPath.includes('data/release-notes/github-ae')) return if (yamlContent.currentWeek) currentWeeksFound.push(relativePath) diff --git a/src/shielding/middleware/handle-invalid-query-string-values.ts b/src/shielding/middleware/handle-invalid-query-string-values.ts index a0fc3fb47aff..9969524f2b9c 100644 --- a/src/shielding/middleware/handle-invalid-query-string-values.ts +++ b/src/shielding/middleware/handle-invalid-query-string-values.ts @@ -19,7 +19,7 @@ const STATSD_KEY = 'middleware.handle_invalid_querystring_values' // that the values of `?platform=...` should be none when the path is // something like `/en/search`. const RECOGNIZED_VALUES = { - platform: allPlatforms, + platform: allPlatforms as string[], tool: Object.keys(allTools), } // So we can look up if a key in the object is actually present diff --git a/src/tools/lib/all-platforms.js b/src/tools/lib/all-platforms.js deleted file mode 100644 index fc716be0dee2..000000000000 --- a/src/tools/lib/all-platforms.js +++ /dev/null @@ -1,2 +0,0 @@ -// all the platforms available for the Platform Picker -export const allPlatforms = ['mac', 'windows', 'linux'] diff --git a/src/tools/lib/all-platforms.ts b/src/tools/lib/all-platforms.ts new file mode 100644 index 000000000000..5681f505b450 --- /dev/null +++ b/src/tools/lib/all-platforms.ts @@ -0,0 +1,9 @@ +/** + * All platforms available for the Platform Picker + */ +export type Platform = 'mac' | 'windows' | 'linux' + +/** + * Array of all supported platforms in the Platform Picker + */ +export const allPlatforms: Platform[] = ['mac', 'windows', 'linux'] diff --git a/src/tools/lib/all-tools.js b/src/tools/lib/all-tools.ts similarity index 70% rename from src/tools/lib/all-tools.js rename to src/tools/lib/all-tools.ts index 9390190511d1..0e56aff7b196 100644 --- a/src/tools/lib/all-tools.js +++ b/src/tools/lib/all-tools.ts @@ -1,5 +1,14 @@ -// all the tools available for the Tool Picker -export const allTools = { +/** + * Interface defining the mapping between tool identifiers and their display names + */ +export interface ToolsMapping { + [key: string]: string +} + +/** + * All the tools available for the Tool Picker + */ +export const allTools: ToolsMapping = { agents: 'Agents', api: 'API', azure_data_studio: 'Azure Data Studio',