From 18b7c2858f9c52a71318ae9d1328a2647d45efbd Mon Sep 17 00:00:00 2001 From: Jamie White Date: Thu, 12 Sep 2024 16:58:26 +0200 Subject: [PATCH 1/4] [WAYP-2783] Add support for a "hidden" tag for api-docs (#2565) This allows us to tag RPCs as `hidden` to prevent them appearing in API docs. Looks like this in practice: rpc UI_LoadProductBanner(UI.LoadProductBannerRequest) returns (UI.LoadProductBannerResponse) { option (google.api.http) = { get: "/waypoint/2023-08-18/namespace/{namespace.id}/ui/product-banner" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: ["WaypointService", "hidden"] }; } --- src/views/open-api-docs-view/utils/get-operation-props.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/views/open-api-docs-view/utils/get-operation-props.ts b/src/views/open-api-docs-view/utils/get-operation-props.ts index 2cb8d8d24b..85a01b9f25 100644 --- a/src/views/open-api-docs-view/utils/get-operation-props.ts +++ b/src/views/open-api-docs-view/utils/get-operation-props.ts @@ -51,6 +51,10 @@ export async function getOperationProps( continue } + if (operation.tags?.includes('hidden')) { + continue + } + // Create a slug for this operation const operationSlug = slugify(operation.operationId) From f4ef779e70768f111db35fac4879539431105fee Mon Sep 17 00:00:00 2001 From: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:43:23 -0400 Subject: [PATCH 2/4] chore: Add version selector back to release notes pages (#2570) --- .../__tests__/is-release-notes-page.test.ts | 60 ------------------- src/lib/docs/is-release-notes-page.ts | 11 ---- src/views/docs-view/server.ts | 7 +-- 3 files changed, 1 insertion(+), 77 deletions(-) delete mode 100644 src/lib/__tests__/is-release-notes-page.test.ts delete mode 100644 src/lib/docs/is-release-notes-page.ts diff --git a/src/lib/__tests__/is-release-notes-page.test.ts b/src/lib/__tests__/is-release-notes-page.test.ts deleted file mode 100644 index 15c7a57d7f..0000000000 --- a/src/lib/__tests__/is-release-notes-page.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { isReleaseNotesPage } from 'lib/docs/is-release-notes-page' - -describe('isReleaseNotesPage', () => { - it('returns true for valid release notes page paths', () => { - const validPaths = [ - '/releases/2022/v220601-1', - '/releases/2021/v210601-2', - '/release-notes/1.2.3', - '/release-notes/2.0.x', - '/release-notes/v2.0.x', - '/boundary/docs/release-notes/v0_15_0', - '/vault/docs/release-notes/1.13.0', - ] - - validPaths.forEach((path) => { - expect(isReleaseNotesPage(path)).toBe(true) - }) - }) - - it('returns false for invalid release notes page paths', () => { - const invalidPaths = [ - '/releases/2022/v220601', - '/releases/2021/v210601', - '/release-notes/1.2', - '/release-notes/2.0', - '/release-notes/2.x', - '/releases/2022/v220601-', - '/releases/2021/v210601-', - '/release-notes/1.2.', - '/release-notes/2.0.', - '/release-notes/2.x.', - '/releases/2022/v220601-1234-5678', - '/releases/2021/v210601-5678-1234', - '/release-notes/1.2.3.4', - '/release-notes/2.0.x.y', - ] - invalidPaths.forEach((path) => { - expect(isReleaseNotesPage(path)).toBe(false) - }) - }) - - it('returns false for non-release notes page paths', () => { - const nonReleaseNotesPaths = [ - '/releases', - '/getting-started', - '/enterprise/v202401-1/migrate', - '/enterprise/v202401-1/releases', - '/waypoint/reference/config', - '/vault/install', - ] - nonReleaseNotesPaths.forEach((path) => { - expect(isReleaseNotesPage(path)).toBe(false) - }) - }) -}) diff --git a/src/lib/docs/is-release-notes-page.ts b/src/lib/docs/is-release-notes-page.ts deleted file mode 100644 index e3c50dfab3..0000000000 --- a/src/lib/docs/is-release-notes-page.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -// Hide the version selector on TFE release notes page, e.g. /terraform/enterprise// or /vault/docs/release-notes/1.13.0 -export const isReleaseNotesPage = (path: string) => - /(\/releases\/\d{4}\/(v\d{6}-\d{1}))$/i.test(path) || - /\/release-notes\/(v\d+[.|_]|(\d+[.|_]))\d+[.|_]([0-9]|x)$/i.test(path) - ? true - : false diff --git a/src/views/docs-view/server.ts b/src/views/docs-view/server.ts index 84424c41ed..b60f05a9f2 100644 --- a/src/views/docs-view/server.ts +++ b/src/views/docs-view/server.ts @@ -43,7 +43,6 @@ import { getDeployPreviewLoader } from './utils/get-deploy-preview-loader' import { getCustomLayout } from './utils/get-custom-layout' import type { DocsViewPropOptions } from './utils/get-root-docs-path-generation-functions' import { DocsViewProps } from './types' -import { isReleaseNotesPage } from 'lib/docs/is-release-notes-page' /** * Returns static generation functions which can be exported from a page to fetch docs data @@ -453,11 +452,7 @@ export function getStaticGenerationFunctions< }, projectName: projectName || null, versions: - !hideVersionSelector && - !isReleaseNotesPage(currentPathUnderProduct) && // toggle version dropdown - hasMeaningfulVersions - ? validVersions - : null, + !hideVersionSelector && hasMeaningfulVersions ? versions : null, } return { From 1512f68a37593f158cce5c25b6ff65dabcfe6935 Mon Sep 17 00:00:00 2001 From: Robert Main <50675045+rmainwork@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:16:25 -0400 Subject: [PATCH 3/4] Remove edit button only for terraform enterprise (#2564) * Simplify isPublicContentRepo expression Since `isHcp` and `isSentinel` aren't used elsewhere, this expression can be simplified down to a one-liner Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> * Use URL to detect terraform enterprise This will hide the "Edit on GitHub" link for terraform enterprise only * Move PTFE detection logic from server.ts to loader * Move private repo logic from server.ts to loader Since logic was implemented in the loader to hide the "Edit this page on GitHub" link for PTFE, it makes sense to move the other (similar) logic there too to keep everything together. Additionally, this also allows the expression to be cleaned up to use `this.opts.product` and `Array.includes()` to detect private repos. --------- Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --- src/views/docs-view/loaders/remote-content.ts | 17 ++++++++++++++++- src/views/docs-view/server.ts | 14 +------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/views/docs-view/loaders/remote-content.ts b/src/views/docs-view/loaders/remote-content.ts index 36b55e43e2..89d9862ba2 100644 --- a/src/views/docs-view/loaders/remote-content.ts +++ b/src/views/docs-view/loaders/remote-content.ts @@ -264,7 +264,22 @@ export default class RemoteContentLoader implements DataLoader { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion versionMetadataList.find((e) => e.version === document.version)! .isLatest - if (isLatest) { + + /** + * We want to show "Edit on GitHub" links for public content repos only. + * Currently, HCP, PTFE and Sentinel docs are stored in private + * repositories. + * + * Note: If we need more granularity here, we could change this to be + * part of `rootDocsPath` configuration in `src/data/.json`. + */ + const isPrivateContentRepo = [ + 'hcp-docs', + 'sentinel', + 'ptfe-releases', + ].includes(this.opts.product) + + if (isLatest && !isPrivateContentRepo) { // GitHub only allows you to modify a file if you are on a branch, not a commit githubFileUrl = `https://github.com/hashicorp/${this.opts.product}/blob/${this.opts.mainBranch}/${document.githubFile}` } diff --git a/src/views/docs-view/server.ts b/src/views/docs-view/server.ts index b60f05a9f2..18fe0184c7 100644 --- a/src/views/docs-view/server.ts +++ b/src/views/docs-view/server.ts @@ -410,19 +410,7 @@ export function getStaticGenerationFunctions< validVersions.length > 0 && (validVersions.length > 1 || validVersions[0].version !== 'v0.0.x') - /** - * We want to show "Edit on GitHub" links for public content repos only. - * Currently, HCP and Sentinel docs are stored in private repositories. - * - * Note: If we need more granularity here, we could change this to be - * part of `rootDocsPath` configuration in `src/data/.json`. - */ - const isHcp = product.slug == 'hcp' - const isSentinel = product.slug == 'sentinel' - const isPublicContentRepo = !isHcp && !isSentinel - if (isPublicContentRepo) { - layoutProps.githubFileUrl = githubFileUrl - } + layoutProps.githubFileUrl = githubFileUrl const { hideVersionSelector, projectName } = options From a612e0bde70509f306929b307e83d62690937aa9 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:50:08 -0400 Subject: [PATCH 4/4] get latest redirects based on nav-data not version metadata (#2563) * split target function into separate file * refactor to fetch with latest sha not ref * update working in test log Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --------- Co-authored-by: Heat Hamilton <55773810+heatlikeheatwave@users.noreply.github.com> --- ...get-latest-content-sha-for-product.test.ts | 33 ++++++++++ .../get-latest-content-sha-for-product.js | 53 ++++++++++++++++ build-libs/redirects.js | 63 +++++++++---------- 3 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 build-libs/__tests__/get-latest-content-sha-for-product.test.ts create mode 100644 build-libs/get-latest-content-sha-for-product.js diff --git a/build-libs/__tests__/get-latest-content-sha-for-product.test.ts b/build-libs/__tests__/get-latest-content-sha-for-product.test.ts new file mode 100644 index 0000000000..d345d39682 --- /dev/null +++ b/build-libs/__tests__/get-latest-content-sha-for-product.test.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import getLatestContentShaForProduct from '../get-latest-content-sha-for-product' +import fetchGithubFile from '@build-libs/fetch-github-file' +import { PRODUCT_REDIRECT_ENTRIES } from '@build-libs/redirects' + +describe('getLatestContentShaForProduct', () => { + PRODUCT_REDIRECT_ENTRIES.forEach(({ repo, path }) => { + it(`fetches the latest SHA for the "${repo}" repo`, async () => { + const latestSha = await getLatestContentShaForProduct(repo) + expect(typeof latestSha).toBe('string') + }) + + if (['hcp-docs', 'sentinel', 'ptfe-releases'].includes(repo)) { + console.log(`Skipping test for private repo "${repo}"`) + } else { + it(`fetches the latest SHA for the "${repo}" repo, then validates the SHA by fetching redirects`, async () => { + const latestSha = await getLatestContentShaForProduct(repo) + expect(typeof latestSha).toBe('string') + const redirectsFileString = await fetchGithubFile({ + owner: 'hashicorp', + repo: repo, + path: path, + ref: latestSha, + }) + expect(typeof redirectsFileString).toBe('string') + }) + } + }) +}) diff --git a/build-libs/get-latest-content-sha-for-product.js b/build-libs/get-latest-content-sha-for-product.js new file mode 100644 index 0000000000..6ea83fe127 --- /dev/null +++ b/build-libs/get-latest-content-sha-for-product.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +/** + * We're using the docs endpoints to fetch the latest SHA, so we use + * the env var for the docs API. + */ +const MKTG_CONTENT_DOCS_API = process.env.MKTG_CONTENT_DOCS_API + +/** + * A map of all possible `product` slugs to known content API endpoints that + * will return an object with a `ref` property that accurately reflects the + * ref from which the latest content was uploaded. + */ +const KNOWN_LATEST_REF_ENDPOINTS = { + boundary: '/api/content/boundary/nav-data/latest/docs', + nomad: '/api/content/nomad/nav-data/latest/docs', + vault: '/api/content/vault/nav-data/latest/docs', + vagrant: '/api/content/vagrant/nav-data/latest/docs', + packer: '/api/content/packer/nav-data/latest/docs', + consul: '/api/content/consul/nav-data/latest/docs', + 'terraform-docs-common': + '/api/content/terraform-docs-common/nav-data/latest/docs', + 'hcp-docs': '/api/content/hcp-docs/nav-data/latest/docs', + 'ptfe-releases': '/api/content/ptfe-releases/nav-data/latest/enterprise', + sentinel: '/api/content/sentinel/nav-data/latest/sentinel', +} + +/** + * Fetch the latest sha from the content API for a given product. + * This relies on known `nav-data` endpoints for each product. + * + * @param {string} product + * @returns {Promise} + */ +async function getLatestContentShaForProduct(product) { + const contentUrl = new URL(MKTG_CONTENT_DOCS_API) + const knownEndpoint = KNOWN_LATEST_REF_ENDPOINTS[product] + if (!knownEndpoint) { + throw new Error( + `getLatestContentShaForProduct failed, with unknown product: ${product}. Please add a known endpoint for this product to KNOWN_LATEST_REF_ENDPOINTS.` + ) + } + contentUrl.pathname = knownEndpoint + const latestSha = await fetch(contentUrl.toString()) + .then((resp) => resp.json()) + .then((json) => json.result.sha) + return latestSha +} + +module.exports = getLatestContentShaForProduct diff --git a/build-libs/redirects.js b/build-libs/redirects.js index d39addf36f..8158cbff3e 100644 --- a/build-libs/redirects.js +++ b/build-libs/redirects.js @@ -10,6 +10,7 @@ const path = require('path') const { isDeployPreview } = require('../src/lib/env-checks') const fetchGithubFile = require('./fetch-github-file') +const getLatestContentShaForProduct = require('./get-latest-content-sha-for-product') const { getTutorialRedirects } = require('./tutorial-redirects') const { getDocsDotHashiCorpRedirects, @@ -26,21 +27,6 @@ const HOSTNAME_MAP = { 'test-st.hashi-mktg.com': 'sentinel', } -/** - * Fetch the latest ref from the content API to ensure the redirects are accurate. - * - * @TODO save the redirects to the content database and expose them directly via the API - */ -async function getLatestContentRefForProduct(product) { - const contentUrl = new URL('https://content.hashicorp.com') - contentUrl.pathname = `/api/content/${product}/version-metadata/latest` - const latestRef = await fetch(contentUrl.toString()) - .then((resp) => resp.json()) - .then((json) => json.result.ref) - - return latestRef -} - /** * Load redirects from a content repository. * @@ -63,13 +49,10 @@ async function getLatestContentRefForProduct(product) { * redirects and return early with an empty array. * * @param {string} repoName The name of the repo, owner is always `hashicorp`. - * @param {string?} redirectsPath Optional, custom path to the redirects file. + * @param {string} redirectsPath Path within the repo to the redirects file. * @returns {Promise} */ -async function getRedirectsFromContentRepo( - repoName, - redirectsPath = 'website/redirects.js' -) { +async function getRedirectsFromContentRepo(repoName, redirectsPath) { /** * Note: These constants are declared for clarity in build context intent. */ @@ -82,12 +65,12 @@ async function getRedirectsFromContentRepo( let redirectsFileString if (isDeveloperBuild) { // For `hashicorp/dev-portal` builds, load redirects remotely - const latestContentRef = await getLatestContentRefForProduct(repoName) + const latestContentSha = await getLatestContentShaForProduct(repoName) redirectsFileString = await fetchGithubFile({ owner: 'hashicorp', repo: repoName, path: redirectsPath, - ref: latestContentRef, + ref: latestContentSha, }) } else if (isLocalContentBuild) { // Load redirects from the filesystem, so that authors can see their changes @@ -106,6 +89,24 @@ async function getRedirectsFromContentRepo( return validRedirects } +/** + * @type {{ repo: string, path: string}[]} An array of redirect + * entries. Each entry specifies a repo and the path within that repo to the + * redirects file. + */ +const PRODUCT_REDIRECT_ENTRIES = [ + { repo: 'boundary', path: 'website/redirects.js' }, + { repo: 'nomad', path: 'website/redirects.js' }, + { repo: 'vault', path: 'website/redirects.js' }, + { repo: 'vagrant', path: 'website/redirects.js' }, + { repo: 'packer', path: 'website/redirects.js' }, + { repo: 'consul', path: 'website/redirects.js' }, + { repo: 'terraform-docs-common', path: 'website/redirects.js' }, + { repo: 'hcp-docs', path: '/redirects.js' }, + { repo: 'ptfe-releases', path: 'website/redirects.js' }, + { repo: 'sentinel', path: 'website/redirects.js' }, +] + async function buildProductRedirects() { // Fetch author-oriented redirects from product repos, // and merge those with dev-oriented redirects from @@ -115,18 +116,11 @@ async function buildProductRedirects() { } const productRedirects = ( - await Promise.all([ - getRedirectsFromContentRepo('boundary'), - getRedirectsFromContentRepo('nomad'), - getRedirectsFromContentRepo('vault'), - getRedirectsFromContentRepo('vagrant'), - getRedirectsFromContentRepo('packer'), - getRedirectsFromContentRepo('consul'), - getRedirectsFromContentRepo('terraform-docs-common'), - getRedirectsFromContentRepo('hcp-docs', '/redirects.js'), - getRedirectsFromContentRepo('ptfe-releases'), - getRedirectsFromContentRepo('sentinel'), - ]) + await Promise.all( + PRODUCT_REDIRECT_ENTRIES.map((entry) => + getRedirectsFromContentRepo(entry.repo, entry.path) + ) + ) ).flat() return productRedirects @@ -407,6 +401,7 @@ async function redirectsConfig() { } module.exports = { + PRODUCT_REDIRECT_ENTRIES, redirectsConfig, splitRedirectsByType, groupSimpleRedirects,