diff --git a/.changeset/techdocs-give-it-all-back.md b/.changeset/techdocs-give-it-all-back.md new file mode 100644 index 0000000000..813f88ba87 --- /dev/null +++ b/.changeset/techdocs-give-it-all-back.md @@ -0,0 +1,16 @@ +--- +'@backstage/techdocs-common': minor +--- + +TechDocs has dropped all support for the long-ago deprecated git-based common +prepares as well as all corresponding values in `backstage.io/techdocs-ref` +annotations. + +Entities whose `backstage.io/techdocs-ref` annotation values still begin with +`github:`, `gitlab:`, `bitbucket:`, or `azure/api:` will no longer be generated +by TechDocs. Be sure to update these values so that they align with their +expected format and your usage of TechDocs. + +For details, see [this explainer on TechDocs ref annotation values][how]. + +[how]: https://backstage.io/docs/features/techdocs/how-to-guides#how-to-understand-techdocs-ref-annotation-values diff --git a/.changeset/techdocs-if-i-could.md b/.changeset/techdocs-if-i-could.md new file mode 100644 index 0000000000..e1b04735ed --- /dev/null +++ b/.changeset/techdocs-if-i-could.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/create-app': patch +--- + +The recommended value for a `backstage.io/techdocs-ref` annotation is now +`dir:.`, indicating "documentation source files are located in the same +directory relative to the catalog entity." Note that `url:` values +are still supported. diff --git a/catalog-info.yaml b/catalog-info.yaml index 0cab0c558a..6de6fd3426 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -15,7 +15,7 @@ metadata: url: https://discord.com/invite/EBHEGzX annotations: github.com/project-slug: backstage/backstage - backstage.io/techdocs-ref: url:https://github.com/backstage/backstage + backstage.io/techdocs-ref: dir:. lighthouse.com/website-url: https://backstage.io spec: type: library diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 5f474624f9..c0c347ac17 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -63,12 +63,18 @@ the `app-config.yaml` of a Backstage installation. # Example: metadata: annotations: - backstage.io/techdocs-ref: url:https://github.com/backstage/backstage/tree/master + backstage.io/techdocs-ref: dir:. ``` -The value of this annotation is a location reference string (see above). If this -annotation is specified, it is expected to point to a repository that the -TechDocs system can read and generate docs from. +The value of this annotation informs _where_ TechDocs source content is stored +so that it can be read and docs can be generated from it. Most commonly, it's +written as a path, relative to the location of the `catalog-info.yaml` itself, +where the associated `mkdocs.yml` file can be found. + +In unusual situations where the documentation for a catalog entity does not live +alongside the entity's source code, the value of this annotation can point to an +absolute URL, matching the location reference string format outlined above, for +example: `url:https://github.com/backstage/backstage/tree/master` ### backstage.io/view-url, backstage.io/edit-url diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 3fdc942ac4..93ce3ad4d9 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -66,9 +66,7 @@ Update your component's entity description by adding the following lines to its ```yaml metadata: annotations: - backstage.io/techdocs-ref: url:https://github.com/org/repo - # Or - # backstage.io/techdocs-ref: url:https://github.com/org/repo/tree/branchName/subFolder + backstage.io/techdocs-ref: dir:. ``` The diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 3f3c92d0de..e1533498d7 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -35,38 +35,64 @@ In your Backstage instance's `app-config.yaml`, set `techdocs.builder` from `'local'` to `'external'`. By doing this, TechDocs will not try to generate docs. Look at [TechDocs configuration](configuration.md) for reference. -## How to use URL Reader in TechDocs Prepare step? +## How to understand techdocs-ref annotation values -If TechDocs is configured to generate docs, it will first download the -repository associated with the `backstage.io/techdocs-ref` annotation defined in -the Entity's `catalog-info.yaml` file. This is also called the +If TechDocs is configured to generate docs, it will first download source files +based on the value of the `backstage.io/techdocs-ref` annotation defined in the +Entity's `catalog-info.yaml` file. This is also called the [Prepare](./concepts.md#techdocs-preparer) step. -There are two kinds of preparers or two ways of downloading these source files +We strongly recommend that the `backstage.io/techdocs-ref` annotation in each +documented catalog entity's `catalog-info.yaml` be set to `dir:.` in almost all +situations. This is because TechDocs is aligned with the "docs like code" +philosophy, whereby documentation should be authored and managed alongside the +source code of the underlying software itself. -- Preparer 1: Doing a `git clone` of the repository (also known as Common Git - Preparer) -- Preparer 2: Downloading an archive.zip or equivalent of the repository (also - known as URL Reader) +When you see `dir:.`, you can translate it to mean: -If `backstage.io/techdocs-ref` is equal to any of these - +- That the documentation source code lives in the same location as the + `catalog-info.yaml` file. +- That, in particular, the `mkdocs.yml` file is a sibling of `catalog-info.yaml` + (meaning, it is in the same directory) +- And that all of the source content of the documentation would be available if + one were to download the directory containing those two files (as well as all + sub-directories). -1. `github:https://githubhost.com/org/repo` -2. `gitlab:https://gitlabhost.com/org/repo` -3. `bitbucket:https://bitbuckethost.com/project/repo` -4. `azure/api:https://azurehost.com/org/project` +The directory tree of the entity would look something like this: -Then Common Git Preparer will be used i.e. a `git clone`. But the URL Reader is -a much faster way to do this step. Convert the `backstage.io/techdocs-ref` -values to the following - +``` +├── catalog-info.yaml +├── mkdocs.yml +└── docs + └── index.md +``` -1. `url:https://githubhost.com/org/repo/tree/` -2. `url:https://gitlabhost.com/org/repo/tree/` -3. `url:https://bitbuckethost.com/project/repo/src/` -4. `url:https://azurehost.com/organization/project/_git/repository` +If, for example, you wanted to keep a lean root directory, you could place your +`mkdocs.yml` file in a subdirectory and update the `backstage.io/techdocs-ref` +annotation value accordingly, e.g. to `dir:./sub-folder`: -Note that you can also provide a path to a non-root directory inside the -repository which contains the `docs/` directory. +``` +├── catalog-info.yaml +└── sub-folder + ├── mkdocs.yml + └── docs + └── index.md +``` + +In rare situations where your TechDocs source content is managed and stored in a +location completely separate from your `catalog-info.yaml`, you can instead +specify a URL location reference, the exact value of which will vary based on +the source code hosting provider. Notice that instead of the `dir:` prefix, the +`url:` prefix is used instead. For example: + +- **GitHub**: `url:https://githubhost.com/org/repo/tree/` +- **GitLab**: `url:https://gitlabhost.com/org/repo/tree/` +- **Bitbucket**: `url:https://bitbuckethost.com/project/repo/src/` +- **Azure**: `url:https://azurehost.com/organization/project/_git/repository` + +Note, just as it's possible to specify a subdirectory with the `dir:` prefix, +you can also provide a path to a non-root directory inside the repository which +contains the `mkdocs.yml` file and `docs/` directory. e.g. `url:https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/examples/documented-component` diff --git a/packages/create-app/templates/default-app/catalog-info.yaml.hbs b/packages/create-app/templates/default-app/catalog-info.yaml.hbs index d66da552b7..265864e235 100644 --- a/packages/create-app/templates/default-app/catalog-info.yaml.hbs +++ b/packages/create-app/templates/default-app/catalog-info.yaml.hbs @@ -6,7 +6,7 @@ metadata: # Example for optional annotations # annotations: # github.com/project-slug: backstage/backstage - # backstage.io/techdocs-ref: url:https://github.com/backstage/backstage + # backstage.io/techdocs-ref: dir:. spec: type: website owner: john@example.com diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 6e845c9797..0c7e5b03c7 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -5,50 +5,24 @@ ```ts /// -import { AzureIntegrationConfig } from '@backstage/integration'; import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; -import { GitHubIntegrationConfig } from '@backstage/integration'; -import { GitLabIntegrationConfig } from '@backstage/integration'; import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "checkoutGitRepository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const checkoutGitRepository: ( - repoUrl: string, - config: Config, - logger: Logger_2, -) => Promise; - -// Warning: (ae-missing-release-tag) "CommonGitPreparer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class CommonGitPreparer implements PreparerBase { - constructor(config: Config, logger: Logger_2); - // Warning: (ae-forgotten-export) The symbol "PreparerResponse" needs to be exported by the entry point index.d.ts - // - // (undocumented) - prepare( - entity: Entity, - options?: { - etag?: string; - }, - ): Promise; -} - // Warning: (ae-missing-release-tag) "DirectoryPreparer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export class DirectoryPreparer implements PreparerBase { constructor(config: Config, _logger: Logger_2, reader: UrlReader); + // Warning: (ae-forgotten-export) The symbol "PreparerResponse" needs to be exported by the entry point index.d.ts + // // (undocumented) prepare( entity: Entity, @@ -121,22 +95,6 @@ export class Generators implements GeneratorBuilder { register(generatorKey: SupportedGeneratorKey, generator: GeneratorBase): void; } -// Warning: (ae-missing-release-tag) "getAzureIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getAzureIntegrationConfig: ( - config: Config, - host: string, -) => AzureIntegrationConfig; - -// Warning: (ae-missing-release-tag) "getDefaultBranch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getDefaultBranch: ( - repositoryUrl: string, - config: Config, -) => Promise; - // Warning: (ae-missing-release-tag) "getDocFilesFromRepository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -151,48 +109,6 @@ export const getDocFilesFromRepository: ( | undefined, ) => Promise; -// Warning: (ae-missing-release-tag) "getGitHost" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function getGitHost(url: string): string; - -// Warning: (ae-missing-release-tag) "getGitHubIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getGitHubIntegrationConfig: ( - config: Config, - host: string, -) => GitHubIntegrationConfig; - -// Warning: (ae-missing-release-tag) "getGitLabIntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getGitLabIntegrationConfig: ( - config: Config, - host: string, -) => GitLabIntegrationConfig; - -// Warning: (ae-missing-release-tag) "getGitRepositoryTempFolder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getGitRepositoryTempFolder: ( - repositoryUrl: string, - config: Config, -) => Promise; - -// Warning: (ae-missing-release-tag) "getGitRepoType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function getGitRepoType(url: string): string; - -// Warning: (ae-missing-release-tag) "getLastCommitTimestamp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getLastCommitTimestamp: ( - repositoryLocation: string, - logger: Logger_2, -) => Promise; - // Warning: (ae-missing-release-tag) "getLocationForEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -201,14 +117,6 @@ export const getLocationForEntity: ( scmIntegration: ScmIntegrationRegistry, ) => ParsedLocationAnnotation; -// Warning: (ae-missing-release-tag) "getTokenForGitRepo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const getTokenForGitRepo: ( - repositoryUrl: string, - config: Config, -) => Promise; - // Warning: (ae-missing-release-tag) "ParsedLocationAnnotation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -311,14 +219,8 @@ export type PublisherType = // Warning: (ae-missing-release-tag) "RemoteProtocol" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public -export type RemoteProtocol = - | 'url' - | 'dir' - | 'github' - | 'gitlab' - | 'file' - | 'azure/api'; +// @public (undocumented) +export type RemoteProtocol = 'url' | 'dir'; // Warning: (ae-missing-release-tag) "TechdocsGenerator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index e9cdade22f..c9d34c36e0 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -46,10 +46,8 @@ "@google-cloud/storage": "^5.6.0", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "fs-extra": "9.1.0", - "git-url-parse": "~11.4.4", "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", @@ -62,7 +60,6 @@ "devDependencies": { "@backstage/cli": "^0.7.6", "@types/fs-extra": "^9.0.5", - "@types/git-url-parse": "^9.0.0", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", "@types/mock-fs": "^4.13.0", diff --git a/packages/techdocs-common/src/default-branch.ts b/packages/techdocs-common/src/default-branch.ts deleted file mode 100644 index f4bca6fd30..0000000000 --- a/packages/techdocs-common/src/default-branch.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import fetch from 'cross-fetch'; -import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { - getGitHubRequestOptions, - getGitLabRequestOptions, - getAzureRequestOptions, -} from '@backstage/integration'; -import { - getGitHost, - getGitRepoType, - getGitHubIntegrationConfig, - getGitLabIntegrationConfig, - getAzureIntegrationConfig, -} from './git-auth'; - -interface IGitlabBranch { - name: string; - merged: boolean; - protected: boolean; - default: boolean; - developers_can_push: boolean; - developers_can_merge: boolean; - can_push: boolean; - web_url: string; - commit: { - author_email: string; - author_name: string; - authored_date: string; - committed_date: string; - committer_email: string; - committer_name: string; - id: string; - short_id: string; - title: string; - message: string; - parent_ids: string[]; - }; -} - -function getGithubApiUrl(config: Config, url: string): URL { - const { resource, owner, name } = parseGitUrl(url); - const providerConfigs = - config.getOptionalConfigArray('integrations.github') ?? []; - - const hostConfig = providerConfigs.filter( - providerConfig => providerConfig.getOptionalString('host') === resource, - ); - - const apiBaseUrl = - hostConfig[0]?.getOptionalString('apiBaseUrl') ?? 'https://api.github.com'; - const apiRepos = 'repos'; - - return new URL(`${apiBaseUrl}/${apiRepos}/${owner}/${name}`); -} - -function getGitlabApiUrl(url: string): URL { - const { protocol, resource, full_name: fullName } = parseGitUrl(url); - const apiProjectsBasePath = 'api/v4/projects'; - const project = encodeURIComponent(fullName); - const branches = 'repository/branches'; - - return new URL( - `${protocol}://${resource}/${apiProjectsBasePath}/${project}/${branches}`, - ); -} - -function getAzureApiUrl(url: string): URL { - const { protocol, resource, organization, owner, name } = parseGitUrl(url); - const apiRepoPath = '_apis/git/repositories'; - const apiVersion = 'api-version=6.0'; - - return new URL( - `${protocol}://${resource}/${organization}/${owner}/${apiRepoPath}/${name}?${apiVersion}`, - ); -} - -async function getGithubDefaultBranch( - repositoryUrl: string, - config: Config, -): Promise { - const path = getGithubApiUrl(config, repositoryUrl).toString(); - const host = getGitHost(repositoryUrl); - - const integrationConfig = getGitHubIntegrationConfig(config, host); - const options = getGitHubRequestOptions(integrationConfig); - - try { - const raw = await fetch(path, options); - - if (!raw.ok) { - throw new Error( - `Failed to load url: ${raw.status} ${raw.statusText}. Make sure you have permission to repository: ${repositoryUrl}`, - ); - } - - const { default_branch: branch } = await raw.json(); - - if (!branch) { - throw new Error('Not found github default branch'); - } - - return branch; - } catch (error) { - throw new Error(`Failed to get github default branch: ${error}`); - } -} - -async function getGitlabDefaultBranch( - repositoryUrl: string, - config: Config, -): Promise { - const path = getGitlabApiUrl(repositoryUrl).toString(); - const host = getGitHost(repositoryUrl); - - const integrationConfig = getGitLabIntegrationConfig(config, host); - const options = getGitLabRequestOptions(integrationConfig); - - try { - const raw = await fetch(path, options); - - if (!raw.ok) { - throw new Error( - `Failed to load url: ${raw.status} ${raw.statusText}. Make sure you have permission to repository: ${repositoryUrl}`, - ); - } - - const result = await raw.json(); - const { name } = (result || []).find( - (branch: IGitlabBranch) => branch.default === true, - ); - - if (!name) { - throw new Error('Not found gitlab default branch'); - } - - return name; - } catch (error) { - throw new Error(`Failed to get gitlab default branch: ${error}`); - } -} - -async function getAzureDefaultBranch( - repositoryUrl: string, - config: Config, -): Promise { - const path = getAzureApiUrl(repositoryUrl).toString(); - const host = getGitHost(repositoryUrl); - - const integrationConfig = getAzureIntegrationConfig(config, host); - const options = getAzureRequestOptions(integrationConfig); - - try { - const urlResponse = await fetch(path, options); - if (!urlResponse.ok) { - throw new Error( - `Failed to load url: ${urlResponse.status} ${urlResponse.statusText}. Make sure you have permission to repository: ${repositoryUrl}`, - ); - } - const urlResult = await urlResponse.json(); - - const idResponse = await fetch(urlResult.url, options); - if (!idResponse.ok) { - throw new Error( - `Failed to load url: ${idResponse.status} ${idResponse.statusText}. Make sure you have permission to repository: ${urlResult.repository.url}`, - ); - } - const idResult = await idResponse.json(); - const name = idResult.defaultBranch; - - if (!name) { - throw new Error('Not found Azure DevOps default branch'); - } - - return name; - } catch (error) { - throw new Error(`Failed to get Azure DevOps default branch: ${error}`); - } -} - -export const getDefaultBranch = async ( - repositoryUrl: string, - config: Config, -): Promise => { - const type = getGitRepoType(repositoryUrl); - - try { - switch (type) { - case 'github': - return await getGithubDefaultBranch(repositoryUrl, config); - case 'gitlab': - return await getGitlabDefaultBranch(repositoryUrl, config); - case 'azure/api': - return await getAzureDefaultBranch(repositoryUrl, config); - - default: - throw new Error('Failed to get repository type'); - } - } catch (error) { - throw error; - } -}; diff --git a/packages/techdocs-common/src/git-auth.ts b/packages/techdocs-common/src/git-auth.ts deleted file mode 100644 index 158c4ebebf..0000000000 --- a/packages/techdocs-common/src/git-auth.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import parseGitUrl from 'git-url-parse'; -import { Config } from '@backstage/config'; -import { - readGitHubIntegrationConfigs, - readGitLabIntegrationConfigs, - readAzureIntegrationConfigs, - GitHubIntegrationConfig, - GitLabIntegrationConfig, - AzureIntegrationConfig, -} from '@backstage/integration'; - -export function getGitHost(url: string): string { - const { resource } = parseGitUrl(url); - return resource; -} - -export function getGitRepoType(url: string): string { - const typeMapping = [ - { url: /github*/g, type: 'github' }, - { url: /gitlab*/g, type: 'gitlab' }, - { url: /azure*/g, type: 'azure/api' }, - ]; - - const type = typeMapping.filter(item => item.url.test(url))[0]?.type; - - return type; -} - -export const getGitHubIntegrationConfig = ( - config: Config, - host: string, -): GitHubIntegrationConfig => { - const allGitHubConfigs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - const gitHubIntegrationConfig = allGitHubConfigs.find(v => v.host === host); - if (!gitHubIntegrationConfig) { - throw new Error(`Unable to locate GitHub integration for the host ${host}`); - } - return gitHubIntegrationConfig; -}; - -export const getGitLabIntegrationConfig = ( - config: Config, - host: string, -): GitLabIntegrationConfig => { - const allGitLabConfigs = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - const gitLabIntegrationConfig = allGitLabConfigs.find(v => v.host === host); - if (!gitLabIntegrationConfig) { - throw new Error(`Unable to locate GitLab integration for the host ${host}`); - } - return gitLabIntegrationConfig; -}; - -export const getAzureIntegrationConfig = ( - config: Config, - host: string, -): AzureIntegrationConfig => { - const allAzureIntegrationConfig = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - const azureIntegrationConfig = allAzureIntegrationConfig.find( - v => v.host === host, - ); - if (!azureIntegrationConfig) { - throw new Error(`Unable to locate Azure integration for the host ${host}`); - } - return azureIntegrationConfig; -}; - -export const getTokenForGitRepo = async ( - repositoryUrl: string, - config: Config, -): Promise => { - const host = getGitHost(repositoryUrl); - const type = getGitRepoType(repositoryUrl); - - try { - switch (type) { - case 'github': - return getGitHubIntegrationConfig(config, host).token; - case 'gitlab': - return getGitLabIntegrationConfig(config, host).token; - case 'azure/api': - return getAzureIntegrationConfig(config, host).token; - default: - throw new Error('Failed to get repository type'); - } - } catch (error) { - throw error; - } -}; diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index bb6461a37c..7a9e4b18be 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -14,26 +14,16 @@ * limitations under the License. */ -import { - Git, - resolveSafeChildPath, - UrlReader, -} from '@backstage/backend-common'; +import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; import { Entity, getEntitySourceLocation, parseLocationReference, } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import fs from 'fs-extra'; -import parseGitUrl from 'git-url-parse'; -import os from 'os'; import path from 'path'; import { Logger } from 'winston'; -import { getDefaultBranch } from './default-branch'; -import { getGitRepoType, getTokenForGitRepo } from './git-auth'; import { PreparerResponse, RemoteProtocol } from './stages/prepare/types'; export type ParsedLocationAnnotation = { @@ -121,9 +111,6 @@ export const getLocationForEntity = ( ); switch (annotation.type) { - case 'github': - case 'gitlab': - case 'azure/api': case 'url': return annotation; case 'dir': @@ -133,124 +120,6 @@ export const getLocationForEntity = ( } }; -export const getGitRepositoryTempFolder = async ( - repositoryUrl: string, - config: Config, -): Promise => { - const parsedGitLocation = parseGitUrl(repositoryUrl); - // removes .git from git location path - parsedGitLocation.git_suffix = false; - - if (!parsedGitLocation.ref) { - parsedGitLocation.ref = await getDefaultBranch( - parsedGitLocation.toString('https'), - config, - ); - } - - return path.join( - // fs.realpathSync fixes a problem with macOS returning a path that is a symlink - fs.realpathSync(os.tmpdir()), - 'backstage-repo', - parsedGitLocation.resource, - parsedGitLocation.owner, - parsedGitLocation.name, - parsedGitLocation.ref, - ); -}; - -export const checkoutGitRepository = async ( - repoUrl: string, - config: Config, - logger: Logger, -): Promise => { - const parsedGitLocation = parseGitUrl(repoUrl); - const repositoryTmpPath = await getGitRepositoryTempFolder(repoUrl, config); - const token = await getTokenForGitRepo(repoUrl, config); - - // Initialize a git client - let git = Git.fromAuth({ logger }); - - // Docs about why username and password are set to these specific values. - // https://isomorphic-git.org/docs/en/onAuth#oauth2-tokens - if (token) { - const type = getGitRepoType(repoUrl); - switch (type) { - case 'github': - git = Git.fromAuth({ - username: 'x-access-token', - password: token, - logger, - }); - parsedGitLocation.token = `x-access-token:${token}`; - break; - case 'gitlab': - git = Git.fromAuth({ - username: 'oauth2', - password: token, - logger, - }); - parsedGitLocation.token = `dummyUsername:${token}`; - parsedGitLocation.git_suffix = true; - break; - case 'azure/api': - git = Git.fromAuth({ - username: 'notempty', - password: token, - logger: logger, - }); - break; - default: - parsedGitLocation.token = `:${token}`; - } - } - - // Pull from repository if it has already been cloned. - if (fs.existsSync(repositoryTmpPath)) { - try { - const currentBranchName = await git.currentBranch({ - dir: repositoryTmpPath, - }); - - await git.fetch({ dir: repositoryTmpPath, remote: 'origin' }); - await git.merge({ - dir: repositoryTmpPath, - theirs: `origin/${currentBranchName}`, - ours: currentBranchName || undefined, - author: { name: 'Backstage TechDocs', email: 'techdocs@backstage.io' }, - committer: { - name: 'Backstage TechDocs', - email: 'techdocs@backstage.io', - }, - }); - return repositoryTmpPath; - } catch (e) { - logger.info( - `Found error "${e.message}" in cached repository "${repoUrl}" when getting latest changes. Removing cached repository.`, - ); - fs.removeSync(repositoryTmpPath); - } - } - - const repositoryCheckoutUrl = parsedGitLocation.toString('https'); - - fs.mkdirSync(repositoryTmpPath, { recursive: true }); - await git.clone({ url: repositoryCheckoutUrl, dir: repositoryTmpPath }); - - return repositoryTmpPath; -}; - -export const getLastCommitTimestamp = async ( - repositoryLocation: string, - logger: Logger, -): Promise => { - const git = Git.fromAuth({ logger }); - const sha = await git.resolveRef({ dir: repositoryLocation, ref: 'HEAD' }); - const commit = await git.readCommit({ dir: repositoryLocation, sha }); - - return commit.commit.committer.timestamp; -}; - export const getDocFilesFromRepository = async ( reader: UrlReader, entity: Entity, diff --git a/packages/techdocs-common/src/index.ts b/packages/techdocs-common/src/index.ts index 881f80cccb..a9a254a1ee 100644 --- a/packages/techdocs-common/src/index.ts +++ b/packages/techdocs-common/src/index.ts @@ -15,5 +15,3 @@ */ export * from './stages'; export * from './helpers'; -export * from './default-branch'; -export * from './git-auth'; diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index a267736bd2..4039d340f2 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -124,7 +124,7 @@ describe('helpers', () => { }; const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'file', + type: 'file' as RemoteProtocol, target: '/home/user/workspace/docs-repository/catalog-info.yaml', }; @@ -147,7 +147,7 @@ describe('helpers', () => { it('should return correct target url for supported hosts', () => { const parsedLocationAnnotation1: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'https://github.com/backstage/backstage.git', }; @@ -156,7 +156,7 @@ describe('helpers', () => { ); const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'https://github.com/org/repo', }; @@ -165,7 +165,7 @@ describe('helpers', () => { ); const parsedLocationAnnotation3: ParsedLocationAnnotation = { - type: 'gitlab', + type: 'gitlab' as RemoteProtocol, target: 'https://gitlab.com/org/repo', }; @@ -174,7 +174,7 @@ describe('helpers', () => { ); const parsedLocationAnnotation4: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', }; @@ -200,7 +200,7 @@ describe('helpers', () => { it('should add repo_url to mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'https://github.com/backstage/backstage', }; @@ -219,7 +219,7 @@ describe('helpers', () => { it('should add repo_url to mkdocs.yml that contains custom yaml tags', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'https://github.com/backstage/backstage', }; @@ -241,7 +241,7 @@ describe('helpers', () => { it('should not override existing repo_url in mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github', + type: 'github' as RemoteProtocol, target: 'https://github.com/neworg/newrepo', }; diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index c10303f29c..7c60c7667f 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -23,7 +23,6 @@ import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; -import { RemoteProtocol } from '../prepare/types'; import { SupportedGeneratorKey } from './types'; // TODO: Implement proper support for more generators. @@ -94,12 +93,12 @@ export const runCommand = async ({ * - (anything that is not valid as described above) * * @param {string} repoUrl URL supposed to be used as repo_url in mkdocs.yml - * @param {RemoteProtocol} locationType Type of source code host - github, gitlab, dir, url, etc. + * @param {string} locationType Type of source code host - github, gitlab, dir, url, etc. * @returns {boolean} */ export const isValidRepoUrlForMkdocs = ( repoUrl: string, - locationType: RemoteProtocol, + locationType: string, ): boolean => { // Trim trailing slash const cleanRepoUrl = repoUrl.replace(/\/$/, ''); @@ -266,6 +265,8 @@ export const patchMkdocsYmlPreBuild = async ( const repoUrl = getRepoUrlFromLocationAnnotation(parsedLocationAnnotation); if (repoUrl !== undefined) { // mkdocs.yml will not build with invalid repo_url. So, make sure it is valid. + // TODO: this is no longer working/meaningful because annotation type is + // now only ever "url" or "dir." Should be re-implemented! if (isValidRepoUrlForMkdocs(repoUrl, parsedLocationAnnotation.type)) { mkdocsYml.repo_url = repoUrl; } diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts b/packages/techdocs-common/src/stages/prepare/commonGit.test.ts deleted file mode 100644 index d1c6c99c6d..0000000000 --- a/packages/techdocs-common/src/stages/prepare/commonGit.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { checkoutGitRepository } from '../../helpers'; -import { CommonGitPreparer } from './commonGit'; - -function normalizePath(path: string) { - return path - .replace(/^[a-z]:/i, '') - .split('\\') - .join('/'); -} - -jest.mock('../../helpers', () => ({ - ...jest.requireActual<{}>('../../helpers'), - checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), - getLastCommitTimestamp: jest.fn(() => 12345678), -})); - -const createMockEntity = (annotations = {}) => { - return { - apiVersion: 'version', - kind: 'TestKind', - metadata: { - name: 'test-component-name', - annotations: { - ...annotations, - }, - }, - }; -}; - -const mockConfig = new ConfigReader({}); - -const logger = getVoidLogger(); - -describe('commonGit preparer', () => { - it('should prepare temp docs path from github repo', async () => { - const preparer = new CommonGitPreparer(mockConfig, logger); - - const mockEntity = createMockEntity({ - 'backstage.io/techdocs-ref': - 'github:https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', - }); - - const { preparedDir } = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(1); - expect(normalizePath(preparedDir)).toEqual( - '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', - ); - }); - - it('should prepare temp docs path from gitlab repo', async () => { - const preparer = new CommonGitPreparer(mockConfig, logger); - - const mockEntity = createMockEntity({ - 'backstage.io/techdocs-ref': - 'gitlab:https://gitlab.com/xesjkeee/go-logger/blob/master/catalog-info.yaml', - }); - - const { preparedDir } = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(2); - expect(normalizePath(preparedDir)).toEqual( - '/tmp/backstage-repo/org/name/branch/catalog-info.yaml', - ); - }); - - it('should prepare temp docs path from azure repo', async () => { - const preparer = new CommonGitPreparer(mockConfig, logger); - - const mockEntity = createMockEntity({ - 'backstage.io/techdocs-ref': - 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', - }); - - const { preparedDir } = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(3); - expect(normalizePath(preparedDir)).toEqual( - '/tmp/backstage-repo/org/name/branch/template.yaml', - ); - }); -}); diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.ts b/packages/techdocs-common/src/stages/prepare/commonGit.ts deleted file mode 100644 index ad70283e78..0000000000 --- a/packages/techdocs-common/src/stages/prepare/commonGit.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { NotModifiedError } from '@backstage/errors'; -import { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; -import parseGitUrl from 'git-url-parse'; -import path from 'path'; -import { Logger } from 'winston'; -import { - checkoutGitRepository, - getLastCommitTimestamp, - parseReferenceAnnotation, -} from '../../helpers'; -import { PreparerBase, PreparerResponse } from './types'; - -export class CommonGitPreparer implements PreparerBase { - private readonly config: Config; - private readonly logger: Logger; - - constructor(config: Config, logger: Logger) { - this.config = config; - this.logger = logger; - } - - async prepare( - entity: Entity, - options?: { etag?: string }, - ): Promise { - this.logger.warn( - `You are using the legacy git preparer in TechDocs for \`${entity.metadata.name}\` which will be removed in near future (March 2021). ` + - `Migrate to URL reader by updating \`backstage.io/techdocs-ref\` annotation in \`catalog-info.yaml\` ` + - `to be prefixed with \`url:\`. Read the migration guide and benefits at https://github.com/backstage/backstage/issues/4409 `, - ); - - const { target } = parseReferenceAnnotation( - 'backstage.io/techdocs-ref', - entity, - ); - - try { - // Update repository or do a fresh clone. - const repoPath = await checkoutGitRepository( - target, - this.config, - this.logger, - ); - // Check if etag has changed for cache invalidation. - const etag = await getLastCommitTimestamp(repoPath, this.logger); - if (options?.etag === etag.toString()) { - throw new NotModifiedError(); - } - - const parsedGitLocation = parseGitUrl(target); - return { - preparedDir: path.join(repoPath, parsedGitLocation.filepath), - etag: etag.toString(), - }; - } catch (error) { - if (error instanceof NotModifiedError) { - this.logger.debug(`Cache is valid for etag ${options?.etag}`); - } else { - this.logger.debug(`Repo checkout failed with error ${error.message}`); - } - throw error; - } - } -} diff --git a/packages/techdocs-common/src/stages/prepare/index.ts b/packages/techdocs-common/src/stages/prepare/index.ts index e683ac8f71..f129bf5315 100644 --- a/packages/techdocs-common/src/stages/prepare/index.ts +++ b/packages/techdocs-common/src/stages/prepare/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ export { DirectoryPreparer } from './dir'; -export { CommonGitPreparer } from './commonGit'; export { UrlPreparer } from './url'; export { Preparers } from './preparers'; export type { PreparerBuilder, PreparerBase, RemoteProtocol } from './types'; diff --git a/packages/techdocs-common/src/stages/prepare/preparers.ts b/packages/techdocs-common/src/stages/prepare/preparers.ts index 5c78d96f15..75c9a7f23b 100644 --- a/packages/techdocs-common/src/stages/prepare/preparers.ts +++ b/packages/techdocs-common/src/stages/prepare/preparers.ts @@ -19,7 +19,6 @@ import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { parseReferenceAnnotation } from '../../helpers'; import { DirectoryPreparer } from './dir'; -import { CommonGitPreparer } from './commonGit'; import { UrlPreparer } from './url'; import { PreparerBase, PreparerBuilder, RemoteProtocol } from './types'; @@ -47,12 +46,6 @@ export class Preparers implements PreparerBuilder { const directoryPreparer = new DirectoryPreparer(config, logger, reader); preparers.register('dir', directoryPreparer); - // Common git preparers will be deprecated soon. - const commonGitPreparer = new CommonGitPreparer(config, logger); - preparers.register('github', commonGitPreparer); - preparers.register('gitlab', commonGitPreparer); - preparers.register('azure/api', commonGitPreparer); - return preparers; } diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/packages/techdocs-common/src/stages/prepare/types.ts index d6f4e79062..5d46b5d235 100644 --- a/packages/techdocs-common/src/stages/prepare/types.ts +++ b/packages/techdocs-common/src/stages/prepare/types.ts @@ -48,14 +48,4 @@ export type PreparerBuilder = { get(entity: Entity): PreparerBase; }; -/** - * Everything except `url` will be deprecated. - * Read more https://github.com/backstage/backstage/issues/4409 - */ -export type RemoteProtocol = - | 'url' - | 'dir' - | 'github' - | 'gitlab' - | 'file' - | 'azure/api'; +export type RemoteProtocol = 'url' | 'dir'; diff --git a/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/catalog-info.yaml index bac97e4bea..df23b90b97 100644 --- a/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/catalog-info.yaml @@ -5,7 +5,7 @@ metadata: description: {{cookiecutter.description | jsonify}} annotations: github.com/project-slug: {{cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} - backstage.io/techdocs-ref: url:{{"https://" + cookiecutter.destination.host + "/" + cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} + backstage.io/techdocs-ref: dir:. spec: type: website lifecycle: experimental diff --git a/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/docs/index.md b/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/docs/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/mkdocs.yml new file mode 100644 index 0000000000..64f1e9ee8e --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/create-react-app/skeleton/mkdocs.yml @@ -0,0 +1,8 @@ +site_name: {{cookiecutter.component_id | jsonify}} +site_description: {{cookiecutter.description | jsonify}} + +nav: + - Introduction: index.md + +plugins: + - techdocs-core diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/skeleton/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/skeleton/catalog-info.yaml index 7233525ac5..8f18e06d7e 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/skeleton/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/skeleton/catalog-info.yaml @@ -5,7 +5,7 @@ metadata: description: {{cookiecutter.description | jsonify}} annotations: github.com/project-slug: {{cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} - backstage.io/techdocs-ref: url:{{"https://" + cookiecutter.destination.host + "/" + cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} + backstage.io/techdocs-ref: dir:. spec: type: documentation lifecycle: experimental diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/skeleton/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/skeleton/catalog-info.yaml index bac97e4bea..df23b90b97 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/skeleton/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/skeleton/catalog-info.yaml @@ -5,7 +5,7 @@ metadata: description: {{cookiecutter.description | jsonify}} annotations: github.com/project-slug: {{cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} - backstage.io/techdocs-ref: url:{{"https://" + cookiecutter.destination.host + "/" + cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} + backstage.io/techdocs-ref: dir:. spec: type: website lifecycle: experimental diff --git a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/skeleton/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/skeleton/catalog-info.yaml index aeae2f816a..4ddec9c032 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/skeleton/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/skeleton/catalog-info.yaml @@ -5,7 +5,7 @@ metadata: description: {{cookiecutter.description | jsonify}} annotations: github.com/project-slug: {{cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} - backstage.io/techdocs-ref: url:{{"https://" + cookiecutter.destination.host + "/" + cookiecutter.destination.owner + "/" + cookiecutter.destination.repo}} + backstage.io/techdocs-ref: dir:. spec: type: service lifecycle: experimental diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index ccdf8d3183..125c18200d 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -4,7 +4,7 @@ metadata: name: documented-component description: A Service with TechDocs documentation annotations: - backstage.io/techdocs-ref: 'url:https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/examples/documented-component' + backstage.io/techdocs-ref: dir:. spec: type: service lifecycle: experimental