diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5368a43186a8..4c223305e759 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -166,4 +166,5 @@ jobs: # Enable debug logging when "Re-run jobs with debug logging" is used in GitHub Actions UI # This will output additional timing and path information to help diagnose timeout issues RUNNER_DEBUG: ${{ runner.debug }} - run: npm test -- src/${{ matrix.name }}/tests/ + VITEST_FLAGS: ${{ matrix.name == 'article-api' && '--no-file-parallelism --maxWorkers=1' || '' }} + run: npm test -- $VITEST_FLAGS src/${{ matrix.name }}/tests/ diff --git a/config/moda/secrets/docs-internal-staging-boxwood/secrets.yml b/config/moda/secrets/docs-internal-staging-boxwood/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-boxwood/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-cedar/secrets.yml b/config/moda/secrets/docs-internal-staging-cedar/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-cedar/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-cypress/secrets.yml b/config/moda/secrets/docs-internal-staging-cypress/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-cypress/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-fir/secrets.yml b/config/moda/secrets/docs-internal-staging-fir/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-fir/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-hemlock/secrets.yml b/config/moda/secrets/docs-internal-staging-hemlock/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-hemlock/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-holly/secrets.yml b/config/moda/secrets/docs-internal-staging-holly/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-holly/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-juniper/secrets.yml b/config/moda/secrets/docs-internal-staging-juniper/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-juniper/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-laurel/secrets.yml b/config/moda/secrets/docs-internal-staging-laurel/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-laurel/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-pine/secrets.yml b/config/moda/secrets/docs-internal-staging-pine/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-pine/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-redwood/secrets.yml b/config/moda/secrets/docs-internal-staging-redwood/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-redwood/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-sequoia/secrets.yml b/config/moda/secrets/docs-internal-staging-sequoia/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-sequoia/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/docs-internal-staging-spruce/secrets.yml b/config/moda/secrets/docs-internal-staging-spruce/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/docs-internal-staging-spruce/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/review-os/secrets.yml b/config/moda/secrets/review-os/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/review-os/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/config/moda/secrets/review/secrets.yml b/config/moda/secrets/review/secrets.yml deleted file mode 100644 index 1cc2ba803f6c..000000000000 --- a/config/moda/secrets/review/secrets.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -secrets: {} diff --git a/content/code-security/tutorials/secret-scanning-partner-program.md b/content/code-security/tutorials/secret-scanning-partner-program.md index e742e073bd1b..027c96d3f2a1 100644 --- a/content/code-security/tutorials/secret-scanning-partner-program.md +++ b/content/code-security/tutorials/secret-scanning-partner-program.md @@ -109,6 +109,7 @@ The list of valid values for `source` are: * Wiki_content * Wiki_commit * Npm +* Manual_submission * Unknown ### Implement signature verification in your secret alert service diff --git a/package-lock.json b/package-lock.json index 8a52ea324048..abf264333371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3018,6 +3018,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3057,6 +3058,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3077,6 +3079,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3097,6 +3100,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3117,6 +3121,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3137,6 +3142,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3157,6 +3163,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3177,6 +3184,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3197,6 +3205,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3217,6 +3226,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3237,6 +3247,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3257,6 +3268,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3277,6 +3289,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3297,6 +3310,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3314,6 +3328,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, "license": "Apache-2.0", "optional": true, "bin": { @@ -8633,6 +8648,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -12589,6 +12605,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, "license": "MIT", "optional": true }, diff --git a/src/app/lib/main-context-adapter.ts b/src/app/lib/main-context-adapter.ts index ee79b2b62693..ccca0c98a17a 100644 --- a/src/app/lib/main-context-adapter.ts +++ b/src/app/lib/main-context-adapter.ts @@ -40,6 +40,7 @@ export function adaptAppRouterContextToMainContext( oldestSupported: '', nextDeprecationDate: '', supported: [], + releasesWithOldestDeprecationDate: [], }, enterpriseServerVersions: [], error: '', diff --git a/src/article-api/README.md b/src/article-api/README.md index acc061b020de..98667b88e554 100644 --- a/src/article-api/README.md +++ b/src/article-api/README.md @@ -23,12 +23,9 @@ The `/api/article` endpoints return information about a page by `pathname`. ### Autogenerated Content Transformers -For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture: +For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture. -#### Current Transformers - -- **REST Transformer** (`rest-transformer.ts`) - Converts REST API operations into markdown, including endpoints, parameters, status codes, and code examples -- **GraphQL Transformer** (`graphql-transformer.ts`) - Converts GraphQL schema documentation into markdown, including queries, mutations, objects, interfaces, enums, unions, input objects, scalars, changelog, and breaking changes +#### Transformers To add a new transformer for other autogenerated content types: 1. Create a new transformer file implementing the `PageTransformer` interface @@ -195,15 +192,7 @@ npm run test -- src/article-api/tests - Team: Docs Engineering -## Transformers - -Currently implemented transformers: -- **REST API transformer** (`rest-transformer.ts`) - Converts REST API autogenerated content -- **GraphQL transformer** (`graphql-transformer.ts`) - Converts GraphQL API autogenerated content -- **Audit logs transformer** (`audit-logs-transformer.ts`) - Converts audit log tables to markdown - ### Known limitations -- Some autogenerated content types don't have transformers yet - Cache invalidation is manual -- No built-in rate limiting +- No built-in rate limiting (uses Fastly instead) - Limited API versioning diff --git a/src/fixtures/fixtures/index.md b/src/fixtures/fixtures/index.md new file mode 100644 index 000000000000..5a6fbc3cbb75 --- /dev/null +++ b/src/fixtures/fixtures/index.md @@ -0,0 +1,6 @@ +--- +title: This is an index page +intro: 'I am missing a children frontmatter property' +versions: + fpt: '*' +--- \ No newline at end of file diff --git a/src/fixtures/fixtures/sample-toc-index.md b/src/fixtures/fixtures/sample-toc-index.md index b9ec21688f23..e73e44e96834 100644 --- a/src/fixtures/fixtures/sample-toc-index.md +++ b/src/fixtures/fixtures/sample-toc-index.md @@ -2,6 +2,6 @@ title: A sample TOC versions: free-pro-team: '*' +children: + - /article-one --- - -{% link_in_list /sample-article %} diff --git a/src/frame/components/context/MainContext.tsx b/src/frame/components/context/MainContext.tsx index 10fcb7a42b58..560aff136184 100644 --- a/src/frame/components/context/MainContext.tsx +++ b/src/frame/components/context/MainContext.tsx @@ -86,6 +86,7 @@ type EnterpriseServerReleases = { oldestSupported: string nextDeprecationDate: string supported: Array + releasesWithOldestDeprecationDate: Array } export type MainContextT = { @@ -193,7 +194,11 @@ export const getMainContext = async (req: any, res: any): Promise // To know whether we need this key, we need to match this // with the business logic in `DeprecationBanner.tsx` which is as follows: - if (req.context.currentVersion.includes(req.context.enterpriseServerReleases.oldestSupported)) { + if ( + req.context.enterpriseServerReleases.releasesWithOldestDeprecationDate.includes( + req.context.currentRelease, + ) + ) { reusables.enterprise_deprecation = { version_was_deprecated: req.context.getDottedData( 'reusables.enterprise_deprecation.version_was_deprecated', @@ -264,6 +269,7 @@ export const getMainContext = async (req: any, res: any): Promise 'oldestSupported', 'nextDeprecationDate', 'supported', + 'releasesWithOldestDeprecationDate', ]), enterpriseServerVersions: req.context.enterpriseServerVersions, error: req.context.error ? req.context.error.toString() : '', diff --git a/src/frame/lib/get-toc-items.ts b/src/frame/lib/get-toc-items.ts deleted file mode 100644 index 4cd40eb0ba8f..000000000000 --- a/src/frame/lib/get-toc-items.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { productMap } from '@/products/lib/all-products' - -interface TocItem { - type: 'category' | 'subcategory' | 'article' - href: string -} - -interface Page { - relativePath: string - markdown: string -} - -const productTOCs = Object.values(productMap) - .filter((product) => !product.external) - .map((product) => product.toc.replace('content/', '')) - -const linkString = /{% [^}]*?link.*? \/(.*?) ?%}/m -const linksArray = new RegExp(linkString.source, 'gm') - -// return an array of objects like { type: 'category|subcategory|article', href: 'path' } -export default function getTocItems(page: Page): TocItem[] | undefined { - // only process product and category tocs - if (!page.relativePath.endsWith('index.md')) return - if (page.relativePath === 'index.md') return - - // ignore content above Table of Contents heading - const pageContent = page.markdown.replace(/[\s\S]*?# Table of contents\n/im, '') - - // find array of TOC link strings - const rawItems = pageContent.match(linksArray) - - // return an empty array if this is a localized page - if (!rawItems) { - return [] - } - - return rawItems - .map((item: string) => { - const match = item.match(linkString) - if (!match) return null - - const tocItem: TocItem = {} as TocItem - - // a product's toc items are always categories - // whereas a category's toc items can be either subcategories or articles - tocItem.type = productTOCs.includes(page.relativePath) - ? 'category' - : page.relativePath.includes('/index.md') - ? 'subcategory' - : 'article' - - tocItem.href = match[1] - - return tocItem - }) - .filter((item): item is TocItem => item !== null) -} diff --git a/src/frame/lib/page.ts b/src/frame/lib/page.ts index b90a82345f57..05de3fef941e 100644 --- a/src/frame/lib/page.ts +++ b/src/frame/lib/page.ts @@ -6,7 +6,6 @@ import getApplicableVersions from '@/versions/lib/get-applicable-versions' import generateRedirectsForPermalinks from '@/redirects/lib/permalinks' import getEnglishHeadings from '@/languages/lib/get-english-headings' import { getAlertTitles } from '@/languages/lib/get-alert-titles' -import getTocItems from './get-toc-items' import Permalink from './permalink' import { renderContent } from '@/content-render/index' import processLearningTracks from '@/learning-track/lib/process-learning-tracks' @@ -100,6 +99,8 @@ class Page { public rawRecommended?: string[] public autogenerated?: string public featuredLinks?: FeaturedLinksExpanded + public children?: string[] + public layout?: string // Derived properties public languageCode!: string @@ -111,7 +112,6 @@ class Page { public documentType: string public applicableVersions: string[] public permalinks: Permalink[] - public tocItems?: any[] public communityRedirect?: CommunityRedirect public detectedPlatforms: string[] = [] public includesPlatformSpecificContent: boolean = false @@ -259,9 +259,13 @@ class Page { this.applicableVersions, ) + // Ensure 'children' frontmatter exists if this is a standard index page if (this.relativePath.endsWith('index.md')) { - // get an array of linked items in product and category TOCs - this.tocItems = getTocItems(this) + if (!this.children && !/(search|early-access)\/.*index.md/.test(this.relativePath)) { + if (this.layout !== 'journey-landing') { + throw new Error(`${this.fullPath} must contain 'children' frontmatter.`) + } + } } // if this is an article and it doesn't have showMiniToc = false, set mini TOC to true diff --git a/src/frame/middleware/context/generic-toc.ts b/src/frame/middleware/context/generic-toc.ts index 7d5bf55da1f5..7a22c9053944 100644 --- a/src/frame/middleware/context/generic-toc.ts +++ b/src/frame/middleware/context/generic-toc.ts @@ -131,7 +131,7 @@ async function getTocItems(node: Tree, context: Context, opts: Options): Promise } return await Promise.all( - node.childPages.filter(filterHidden).map(async (child) => { + (node.childPages || []).filter(filterHidden).map(async (child) => { const { page } = child const title = await page.renderProp('rawTitle', context, { textOnly: true }) const octicon = page.octicon ?? null diff --git a/src/frame/tests/page.ts b/src/frame/tests/page.ts index 03f8821b0798..266babf20100 100644 --- a/src/frame/tests/page.ts +++ b/src/frame/tests/page.ts @@ -439,6 +439,18 @@ describe('catches errors thrown in Page class', () => { await expect(getPage).rejects.toThrowError('versions') }) + test('missing children frontmatter in index file', async () => { + async function getPage() { + return await Page.init({ + relativePath: 'index.md', + basePath: path.join(__dirname, '../../../src/fixtures/fixtures'), + languageCode: 'en', + }) + } + + await expect(getPage).rejects.toThrowError(/must contain 'children' frontmatter/) + }) + test('English page with a version in frontmatter that its parent product is not available in', async () => { async function getPage() { return await Page.init({ diff --git a/src/versions/components/DeprecationBanner.tsx b/src/versions/components/DeprecationBanner.tsx index ed989fb15344..66b72cc5230a 100644 --- a/src/versions/components/DeprecationBanner.tsx +++ b/src/versions/components/DeprecationBanner.tsx @@ -9,8 +9,9 @@ import styles from './DeprecationBanner.module.scss' export const DeprecationBanner = () => { const { data, enterpriseServerReleases } = useMainContext() const { currentVersion } = useVersion() + const currentRelease = currentVersion.replace('enterprise-server@', '') - if (!currentVersion.includes(enterpriseServerReleases.oldestSupported)) { + if (!enterpriseServerReleases.releasesWithOldestDeprecationDate.includes(currentRelease)) { return null } diff --git a/src/versions/lib/enterprise-server-releases.d.ts b/src/versions/lib/enterprise-server-releases.d.ts index e094961c96cb..cab010afad08 100644 --- a/src/versions/lib/enterprise-server-releases.d.ts +++ b/src/versions/lib/enterprise-server-releases.d.ts @@ -42,6 +42,7 @@ export const oldestSupported: string export const dates: Dates export const nextDeprecationDate: string export const isOldestReleaseDeprecated: boolean +export const releasesWithOldestDeprecationDate: string[] export const deprecatedOnNewSite: string[] export const deprecatedReleasesWithLegacyFormat: string[] export const deprecatedReleasesWithNewFormat: string[] diff --git a/src/versions/lib/enterprise-server-releases.ts b/src/versions/lib/enterprise-server-releases.ts index 560fd836d7d3..7a4772b7b417 100644 --- a/src/versions/lib/enterprise-server-releases.ts +++ b/src/versions/lib/enterprise-server-releases.ts @@ -127,6 +127,12 @@ export const isOldestReleaseDeprecated = nextDeprecationDate ? new Date() > new Date(nextDeprecationDate) : false +// Find any other releases that may share the oldest deprecation date +// We'll want to display the deprecation banner on all of these releases (not just oldest) +export const releasesWithOldestDeprecationDate = Object.entries(dates) + .filter(([, versionData]) => versionData.deprecationDate === nextDeprecationDate) + .map(([version]) => version) + // Filtered version arrays for different use cases export const deprecatedOnNewSite = deprecated.filter((version) => versionSatisfiesRange(version, '>=2.13'), @@ -210,6 +216,7 @@ export default { oldestSupported, nextDeprecationDate, isOldestReleaseDeprecated, + releasesWithOldestDeprecationDate, deprecatedOnNewSite, dates, firstVersionDeprecatedOnNewSite, diff --git a/src/versions/tests/enterprise-versions.ts b/src/versions/tests/enterprise-versions.ts index 9a3e6cbc3a2b..c4eae998974e 100644 --- a/src/versions/tests/enterprise-versions.ts +++ b/src/versions/tests/enterprise-versions.ts @@ -2,8 +2,15 @@ import { describe, expect, test } from 'vitest' import patterns from '@/frame/lib/patterns' import EnterpriseServerReleases from '@/versions/lib/enterprise-server-releases' -const { supported, deprecated, all, latest, oldestSupported, nextDeprecationDate } = - EnterpriseServerReleases +const { + supported, + deprecated, + all, + latest, + oldestSupported, + nextDeprecationDate, + releasesWithOldestDeprecationDate, +} = EnterpriseServerReleases describe('enterpriseServerReleases module', () => { test('includes an array of `supported` versions', async () => { @@ -37,4 +44,10 @@ describe('enterpriseServerReleases module', () => { test('has a `nextDeprecationDate` property', async () => { expect(nextDeprecationDate).toMatch(patterns.ymd) }) + + test('has a `releasesWithOldestDeprecationDate` property', async () => { + expect(Array.isArray(releasesWithOldestDeprecationDate)).toBe(true) + expect(releasesWithOldestDeprecationDate.length).toBeGreaterThan(0) + expect(releasesWithOldestDeprecationDate).toContain(oldestSupported) + }) })