Merge pull request #6639 from backstage/techdocs/ref-follow-ups

[TechDocs] Remove deprecated preparer support and document ref values
This commit is contained in:
Eric Peterson
2021-08-03 09:21:38 +02:00
committed by GitHub
27 changed files with 119 additions and 809 deletions
+16
View File
@@ -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
+10
View File
@@ -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:<location>` values
are still supported.
+1 -1
View File
@@ -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
@@ -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
@@ -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
+49 -23
View File
@@ -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/<branch_name>`
2. `url:https://gitlabhost.com/org/repo/tree/<branch_name>`
3. `url:https://bitbuckethost.com/project/repo/src/<branch_name>`
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/<branch_name>`
- **GitLab**: `url:https://gitlabhost.com/org/repo/tree/<branch_name>`
- **Bitbucket**: `url:https://bitbuckethost.com/project/repo/src/<branch_name>`
- **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`
@@ -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
+4 -102
View File
@@ -5,50 +5,24 @@
```ts
/// <reference types="node" />
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<string>;
// 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<PreparerResponse>;
}
// 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<string>;
// 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<PreparerResponse>;
// 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<string>;
// 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<number>;
// 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<string | undefined>;
// 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)
//
-3
View File
@@ -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",
@@ -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<string> {
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<string> {
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<string> {
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<string> => {
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;
}
};
-109
View File
@@ -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<string | undefined> => {
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;
}
};
+1 -132
View File
@@ -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<string> => {
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<string> => {
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<number> => {
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,
-2
View File
@@ -15,5 +15,3 @@
*/
export * from './stages';
export * from './helpers';
export * from './default-branch';
export * from './git-auth';
@@ -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',
};
@@ -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;
}
@@ -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',
);
});
});
@@ -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<PreparerResponse> {
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;
}
}
}
@@ -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';
@@ -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;
}
@@ -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';
@@ -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
@@ -0,0 +1,8 @@
site_name: {{cookiecutter.component_id | jsonify}}
site_description: {{cookiecutter.description | jsonify}}
nav:
- Introduction: index.md
plugins:
- techdocs-core
@@ -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
@@ -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
@@ -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
@@ -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