Merge branch 'master' into fix-catalog-import

This commit is contained in:
Adam Harvey
2020-12-29 11:44:07 -05:00
committed by GitHub
128 changed files with 1954 additions and 1145 deletions
-6
View File
@@ -1,6 +0,0 @@
---
'@backstage/core-api': patch
'@backstage/core': patch
---
Add `BackstageRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-register-component': patch
---
Register component plugin is deprecated in favor of @backstage/plugin-catalog-import
-9
View File
@@ -1,9 +0,0 @@
---
'@backstage/plugin-catalog': patch
---
Add `CatalogIndexPage` and `CatalogEntityPage`, two new extensions that replace the existing `Router` component.
Add `EntityLayout` to replace `EntityPageLayout`, using children instead of an element property, and allowing for collection of all `RouteRef` mount points used within tabs.
Add `EntitySwitch` to be used to select components based on entity data, along with accompanying `isKind`, `isNamespace`, and `isComponentType` filters.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-api-docs': patch
---
Link register API to catalog-import plugin
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-common': patch
'@backstage/techdocs-common': patch
'@backstage/plugin-scaffolder-backend': patch
---
Moving the Git actions to isomorphic-git instead of the node binding version of nodegit
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Tweaked development log formatter to include extra fields at the end of each log line
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-fossa': patch
---
Bump dependency versions of @backstage/core, cli and test-utils
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-auth-backend': patch
---
fix bug in token expiration date
+1
View File
@@ -134,6 +134,7 @@ neuro
newrelic
nginx
Niklas
nodegit
nohoist
nonces
npm
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
- uses: Klemensas/action-autotag@1.2.3
if: matrix.node-version == '12.x'
with:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GITHUB_TOKEN: '${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}'
package_root: 'packages/core'
tag_prefix: 'v'
+33 -21
View File
@@ -6,19 +6,19 @@ Therefore we want to create strong community of contributors -- all working toge
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❤️
Backstage is released under the Apache2.0 License, and original creations contributed to this repo are accepted under the same license.
Backstage is released under the Apache 2.0 License, and original creations contributed to this repo are accepted under the same license.
# Types of Contributions
## Types of Contributions
## Report bugs
### Report bugs
No one likes bugs. Report bugs as an issue [here](https://github.com/backstage/backstage/issues/new?template=bug_template.md).
## Fix bugs or build new features
### Fix bugs or build new features
Look through the GitHub issues for [bugs](https://github.com/backstage/backstage/labels/bugs), [good first issues](https://github.com/backstage/backstage/labels/good%20first%20issue) or [help wanted](https://github.com/backstage/backstage/labels/help%20wanted).
## Build a plugin
### Build a plugin
The value of Backstage grows with every new plugin that gets added. Wouldn't it be fantastic if there was a plugin for every infrastructure project out there? We think so. And we would love your help.
@@ -26,27 +26,27 @@ A great reference example of a plugin can be found on [our blog](https://backsta
What kind of plugins should/could be created? Some inspiration from the 120+ plugins that we have developed inside Spotify can be found [here](https://backstage.io/demos), but we will keep a running list of suggestions labeled with [[plugin]](https://github.com/backstage/backstage/labels/plugin).
## Suggesting a plugin
### Suggesting a plugin
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new Issue](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
## Adding Non-code Contributions
### Adding Non-code Contributions
Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/backstage/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more.
## Write Documentation
### Write Documentation
The current documentation is very limited. Help us make the `/docs` folder come alive.
## Contribute to Storybook
### Contribute to Storybook
We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://backstage.io/storybook).
Either help us [create new components](https://github.com/backstage/backstage/labels/help%20wanted) or improve stories for the existing ones (look for files with `*.stories.tsx`).
## Submit Feedback
### Submit Feedback
The best way to send feedback is to file [an issue](https://github.com/backstage/backstage/issues).
@@ -58,47 +58,59 @@ If you are proposing a feature:
- Remember that this is a volunteer-driven project, and that contributions
are welcome :)
## Add your company to ADOPTERS
### Add your company to ADOPTERS
Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project.
# Get Started!
## Get Started!
So...feel ready to jump in? Let's do this. 👏🏻💯
Start by reading our [Getting Started](https://backstage.io/docs/getting-started/) page. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
# Coding Guidelines
## Coding Guidelines
All code is formatted with `prettier` using the configuration in the repo. If possible we recommend configuring your editor to format automatically, but you can also use the `yarn prettier --write <file>` command to format files.
A consistent coding style is included via [EditorConfig](https://editorconfig.org/) with the file [`.editorconfig`](.editorconfig) at the root of the repo. Depending on your editor of choice, it will either support it out of the box or you can [download a plugin](https://editorconfig.org/#download) for the config to be applied.
If you're contributing to the backend or CLI tooling, be mindful of cross-platform support. [This](https://shapeshed.com/writing-cross-platform-node/) blog post is a good guide of what to keep in mind when writing cross-platform NodeJS.
Also be sure to skim through our [ADRs](https://github.com/backstage/backstage/tree/master/docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](https://github.com/backstage/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for.
If there are any updates in `markdown` file please make sure to run `yarn run lint:docs`. Though it is checked on `lint-staged`. It is required to install [vale](https://docs.errata.ai/vale/install) separately and make sure it is accessed by global command.
# Creating Changesets
## Creating Changesets
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. It helps us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy. it helps when contributors include changesets with their pull requests.
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. They help us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy, it helps when contributors include changesets with their pull requests.
## To create a changeset
### When to use a changeset?
Any time a patch, minor, or major change aligning to [Semantic Versioning](https://semver.org) is made to any published package in `packages/` or `plugins/`, a changeset should be used. It helps to align your change to the [Backstage stability index](https://backstage.io/docs/overview/stability-index) for the package you are changing, for example, when to provide additional clarity on deprecation or impacting changes which will then be included into CHANGELOGs.
In general, changesets are not needed for the documentation, build utilities, contributed samples in `contrib/`, or the [example `packages/app`](packages/app).
### How to create a changeset
1. Run `yarn changeset`
2. Select which packages you want to include a changeset for
3. Select impact of change that you're introducing (minor, major or patch)
3. Select impact of change that you're introducing (patch, minor, or major)
4. Add generated changeset to Git
5. Push the commit with your changeset to the branch associated with your PR
6. Accept our gratitude for making the release process easier on the maintainer
6. Accept our gratitude for making the release process easier on the maintainers
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in changesets repository.
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository.
# Code of Conduct
## Merging to Master
For those contributors who have earned write access to the repository, when a pull request is approved, in general we prefer the author of the PR to perform the merge themselves. This allows them to own accountability for the change and they likely know best how or when to address pending fixes or additional follow-ups. In this way, we all help contribute to the project's successful outcomes.
## Code of Conduct
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md
# Security Issues?
## Security Issues?
See [SECURITY](SECURITY.md).
+10 -14
View File
@@ -17,22 +17,18 @@ const Background = props => {
<Block small className="stripe-bottom bg-black-grey">
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.TextBox>
<Block.Title>See us in action.</Block.Title>
<Block.Title>See us in action</Block.Title>
<Block.Paragraph>
To illustrate the potential of Backstage, were showing you{' '}
<a href="https://labs.spotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
how we use it here at Spotify
To explore the UI and basic features of Backstage firsthand, go
to: <a href="https://demo.backstage.io">demo.backstage.io</a>.
</Block.Paragraph>
<Block.Paragraph>
Watch the videos below to get an introduction to Backstage and to
see how we use different plugins to customize{' '}
<a href="https://engineering.atspotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
our internal version of Backstage at Spotify
</a>
. The videos below feature the custom plugins and templates that
we built for our internal version of Backstage. You can use
Backstage to build the developer portal you want integrating the
tools that you use in your own infrastructure ecosystem. (Or get
started by building an open source plugin for Backstage that
everyone can use, like our{' '}
<a href="https://backstage.io/blog/2020/04/06/lighthouse-plugin">
Lighthouse Plugin
</a>
.)
.
</Block.Paragraph>
</Block.TextBox>
<Block.Graphics>
+1
View File
@@ -45,6 +45,7 @@
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.3",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"knex": "^0.21.6",
"lodash": "^4.17.15",
"logform": "^2.1.1",
+1
View File
@@ -24,3 +24,4 @@ export * from './reading';
export * from './service';
export * from './paths';
export * from './hot';
export * from './scm';
@@ -17,19 +17,23 @@ import * as winston from 'winston';
import { TransformableInfo } from 'logform';
const coloredTemplate = (info: TransformableInfo) => {
const { timestamp, level, message, plugin, service } = info;
const { timestamp, level, message, plugin, service, ...fields } = info;
const colorizer = winston.format.colorize();
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
return `${timestampColor} ${prefixColor} ${level} ${message}`;
const extraFields = Object.entries(fields)
.map(([key, value]) => `${colorizer.colorize('field', `${key}`)}=${value}`)
.join(' ');
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
};
export const coloredFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.colorize({
colors: { timestamp: 'dim', prefix: 'blue' },
colors: { timestamp: 'dim', prefix: 'blue', field: 'cyan' },
}),
winston.format.printf(coloredTemplate),
);
+321
View File
@@ -0,0 +1,321 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
jest.mock('isomorphic-git');
jest.mock('isomorphic-git/http/node');
jest.mock('fs-extra');
import * as isomorphic from 'isomorphic-git';
import { Git } from './git';
import http from 'isomorphic-git/http/node';
import fs from 'fs-extra';
describe('Git', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('add', () => {
it('should call isomorphic-git add with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const filepath = 'mockfile/path';
await git.add({ dir, filepath });
expect(isomorphic.add).toHaveBeenCalledWith({
fs,
dir,
filepath,
});
});
});
describe('addRemote', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const remote = 'origin';
const url = 'git@github.com/something/sads';
await git.addRemote({ dir, remote, url });
expect(isomorphic.addRemote).toHaveBeenCalledWith({
fs,
dir,
remote,
url,
});
});
});
describe('commit', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const message = 'Inital Commit';
const author = {
name: 'author',
email: 'test@backstage.io',
};
const committer = {
name: 'comitter',
email: 'test@backstage.io',
};
await git.commit({ dir, message, author, committer });
expect(isomorphic.commit).toHaveBeenCalledWith({
fs,
dir,
message,
author,
committer,
});
});
});
describe('clone', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const url = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.clone({ url, dir });
expect(isomorphic.clone).toHaveBeenCalledWith({
fs,
http,
url,
dir,
singleBranch: true,
depth: 1,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const url = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.clone({ url, dir });
const { onAuth } = ((isomorphic.clone as unknown) as jest.Mock<
typeof isomorphic['clone']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('currentBranch', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const fullName = true;
const git = Git.fromAuth({});
await git.currentBranch({ dir, fullName });
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
fs,
dir,
fullname: true,
});
await git.currentBranch({ dir });
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
fs,
dir,
fullname: false,
});
});
});
describe('fetch', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const remote = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.fetch({ remote, dir });
expect(isomorphic.fetch).toHaveBeenCalledWith({
fs,
http,
remote,
dir,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const remote = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.fetch({ remote, dir });
const { onAuth } = ((isomorphic.fetch as unknown) as jest.Mock<
typeof isomorphic['fetch']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('init', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const git = Git.fromAuth({});
await git.init({ dir });
expect(isomorphic.init).toHaveBeenCalledWith({
fs,
dir,
});
});
});
describe('merge', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const author = {
name: 'author',
email: 'test@backstage.io',
};
const committer = {
name: 'comitter',
email: 'test@backstage.io',
};
const theirs = 'master';
const ours = 'production';
const git = Git.fromAuth({});
await git.merge({ dir, theirs, ours, author, committer });
expect(isomorphic.merge).toHaveBeenCalledWith({
fs,
dir,
ours,
theirs,
author,
committer,
});
});
});
describe('push', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const remote = 'origin';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.push({ dir, remote });
expect(isomorphic.push).toHaveBeenCalledWith({
fs,
http,
remote,
dir,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const remote = 'origin';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.push({ remote, dir });
const { onAuth } = ((isomorphic.push as unknown) as jest.Mock<
typeof isomorphic['push']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('readCommit', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const sha = 'as43bd7';
const git = Git.fromAuth({});
await git.readCommit({ dir, sha });
expect(isomorphic.readCommit).toHaveBeenCalledWith({
fs,
dir,
oid: sha,
});
});
});
describe('resolveRef', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const ref = 'as43bd7';
const git = Git.fromAuth({});
await git.resolveRef({ dir, ref });
expect(isomorphic.resolveRef).toHaveBeenCalledWith({
fs,
dir,
ref,
});
});
});
});
+251
View File
@@ -0,0 +1,251 @@
/*
* Copyright 2020 Spotify AB
*
* 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 git, {
ProgressCallback,
MergeResult,
ReadCommitResult,
} from 'isomorphic-git';
import http from 'isomorphic-git/http/node';
import fs from 'fs-extra';
import { Logger } from 'winston';
/*
provider username password
GitHub token 'x-oauth-basic'
GitHub App token 'x-access-token'
BitBucket 'x-token-auth' token
GitLab 'oauth2' token
From : https://isomorphic-git.org/docs/en/onAuth
Azure 'notempty' token
*/
export class Git {
private constructor(
private readonly config: {
username?: string;
password?: string;
logger?: Logger;
},
) {}
async add({
dir,
filepath,
}: {
dir: string;
filepath: string;
}): Promise<void> {
this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`);
return git.add({ fs, dir, filepath });
}
async addRemote({
dir,
url,
remote,
}: {
dir: string;
remote: string;
url: string;
}): Promise<void> {
this.config.logger?.info(
`Creating new remote {dir=${dir},remote=${remote},url=${url}}`,
);
return git.addRemote({ fs, dir, remote, url });
}
async commit({
dir,
message,
author,
committer,
}: {
dir: string;
message: string;
author: { name: string; email: string };
committer: { name: string; email: string };
}): Promise<string> {
this.config.logger?.info(
`Committing file to repo {dir=${dir},message=${message}}`,
);
return git.commit({ fs, dir, message, author, committer });
}
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
http,
url,
dir,
singleBranch: true,
depth: 1,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: this.onAuth,
});
}
// https://isomorphic-git.org/docs/en/currentBranch
async currentBranch({
dir,
fullName,
}: {
dir: string;
fullName?: boolean;
}): Promise<string | undefined> {
const fullname = fullName ?? false;
return git.currentBranch({ fs, dir, fullname }) as Promise<
string | undefined
>;
}
// https://isomorphic-git.org/docs/en/fetch
async fetch({
dir,
remote,
}: {
dir: string;
remote?: string;
}): Promise<void> {
const remoteValue = remote ?? 'origin';
this.config.logger?.info(
`Fetching remote=${remoteValue} for repository {dir=${dir}}`,
);
await git.fetch({
fs,
http,
dir,
remote: remoteValue,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: this.onAuth,
});
}
async init({ dir }: { dir: string }): Promise<void> {
this.config.logger?.info(`Init git repository {dir=${dir}}`);
return git.init({
fs,
dir,
});
}
// https://isomorphic-git.org/docs/en/merge
async merge({
dir,
theirs,
ours,
author,
committer,
}: {
dir: string;
theirs: string;
ours?: string;
author: { name: string; email: string };
committer: { name: string; email: string };
}): Promise<MergeResult> {
this.config.logger?.info(
`Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`,
);
// If ours is undefined, current branch is used.
return git.merge({
fs,
dir,
ours,
theirs,
author,
committer,
});
}
async push({ dir, remote }: { dir: string; remote: string }) {
this.config.logger?.info(
`Pushing directory to remote {dir=${dir},remote=${remote}}`,
);
return git.push({
fs,
dir,
http,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
remote: remote,
onAuth: this.onAuth,
});
}
// https://isomorphic-git.org/docs/en/readCommit
async readCommit({
dir,
sha,
}: {
dir: string;
sha: string;
}): Promise<ReadCommitResult> {
return git.readCommit({ fs, dir, oid: sha });
}
// https://isomorphic-git.org/docs/en/resolveRef
async resolveRef({
dir,
ref,
}: {
dir: string;
ref: string;
}): Promise<string> {
return git.resolveRef({ fs, dir, ref });
}
private onAuth = () => ({
username: this.config.username,
password: this.config.password,
});
private onProgressHandler = (): ProgressCallback => {
let currentPhase = '';
return event => {
if (currentPhase !== event.phase) {
currentPhase = event.phase;
this.config.logger?.info(event.phase);
}
const total = event.total
? `${Math.round((event.loaded / event.total) * 100)}%`
: event.loaded;
this.config.logger?.debug(`status={${event.phase},total={${total}}}`);
};
};
static fromAuth = ({
username,
password,
logger,
}: {
username?: string;
password?: string;
logger?: Logger;
}) => new Git({ username, password, logger });
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export { Git } from './git';
+6
View File
@@ -1,5 +1,11 @@
# @backstage/cli
## 0.4.4
### Patch Changes
- d45efbc9b: Fix typo in .app.listen.port config schema
## 0.4.3
### Patch Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.4.3",
"version": "0.4.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -113,8 +113,8 @@
"devDependencies": {
"@backstage/backend-common": "^0.4.1",
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.2",
"@backstage/dev-utils": "^0.1.6",
"@backstage/core": "^0.4.3",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@backstage/theme": "^0.2.2",
"@types/diff": "^4.0.2",
@@ -182,7 +182,7 @@
"visibility": "frontend",
"description": "The host that the frontend should be bound to. Only used for local development."
},
"post": {
"port": {
"type": "number",
"visibility": "frontend",
"description": "The port that the frontend should be bound to. Only used for local development."
@@ -14,20 +14,27 @@
* limitations under the License.
*/
import * as os from 'os';
import * as path from 'path';
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
describe('LinkedPackageResolvePlugin', () => {
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
it('should re-write paths for external packages', () => {
const plugin = new LinkedPackageResolvePlugin('/root/repo/node_modules', [
{
name: 'a',
location: '/root/external-a',
},
{
name: '@s/b',
location: '/root/external-b',
},
]);
const plugin = new LinkedPackageResolvePlugin(
path.resolve(root, 'repo/node_modules'),
[
{
name: 'a',
location: path.resolve(root, 'external-a'),
},
{
name: '@s/b',
location: path.resolve(root, 'external-b'),
},
],
);
const tapAsync = jest.fn();
const doResolve = jest.fn();
@@ -50,10 +57,10 @@ describe('LinkedPackageResolvePlugin', () => {
const callbackX = jest.fn();
tap(
{
request: '/root/repo/package/x/src/module.ts',
path: '/root/repo/package/x/src',
request: path.resolve(root, 'repo/package/x/src/module.ts'),
path: path.resolve(root, 'repo/package/x/src'),
context: {
issuer: '/root/repo/package/x/src/index.ts',
issuer: path.resolve(root, 'repo/package/x/src/index.ts'),
},
},
'some-context',
@@ -81,10 +88,10 @@ describe('LinkedPackageResolvePlugin', () => {
const callbackA = jest.fn();
tap(
{
request: '/root/external-a/src/module.ts',
path: '/root/external-a/src',
request: path.resolve(root, 'external-a/src/module.ts'),
path: path.resolve(root, 'external-a/src'),
context: {
issuer: '/root/external-a/src/index.ts',
issuer: path.resolve(root, 'external-a/src/index.ts'),
},
},
'some-context',
@@ -95,13 +102,16 @@ describe('LinkedPackageResolvePlugin', () => {
expect(doResolve).toHaveBeenCalledWith(
resolver.hooks.resolve,
{
request: '/root/external-a/src/module.ts',
path: '/root/repo/node_modules/a/src',
request: path.resolve(root, 'external-a/src/module.ts'),
path: path.resolve(root, 'repo/node_modules/a/src'),
context: {
issuer: '/root/repo/node_modules/a/src/index.ts',
issuer: path.resolve(root, 'repo/node_modules/a/src/index.ts'),
},
},
'resolve /root/external-a/src/module.ts in /root/repo/node_modules/a',
`resolve ${path.resolve(
root,
'external-a/src/module.ts',
)} in ${path.resolve(root, 'repo/node_modules/a')}`,
'some-context',
callbackA,
);
@@ -110,8 +120,8 @@ describe('LinkedPackageResolvePlugin', () => {
const callbackB = jest.fn();
tap(
{
request: '/root/external-b/src/module.ts',
path: '/root/external-b/src',
request: path.resolve(root, 'external-b/src/module.ts'),
path: path.resolve(root, 'external-b/src'),
context: {
issuer: false,
},
@@ -124,13 +134,16 @@ describe('LinkedPackageResolvePlugin', () => {
expect(doResolve).toHaveBeenLastCalledWith(
resolver.hooks.resolve,
{
request: '/root/external-b/src/module.ts',
path: '/root/repo/node_modules/@s/b/src',
request: path.resolve(root, 'external-b/src/module.ts'),
path: path.resolve(root, 'repo/node_modules/@s/b/src'),
context: {
issuer: false,
},
},
'resolve /root/external-b/src/module.ts in /root/repo/node_modules/@s/b',
`resolve ${path.resolve(
root,
'external-b/src/module.ts',
)} in ${path.resolve(root, 'repo/node_modules/@s/b')}`,
'some-context',
callbackB,
);
+8
View File
@@ -1,5 +1,13 @@
# @backstage/core-api
## 0.2.8
### Patch Changes
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
- 86c3c652a: Deprecate `RouteRef` path parameter and member, and remove deprecated `routeRef.createSubRouteRef`.
- 27f2af935: Delay auth loginPopup close to avoid race condition with callers of authFlowHelpers.
## 0.2.7
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.2.7",
"version": "0.2.8",
"private": false,
"publishConfig": {
"access": "public",
@@ -43,7 +43,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/cli": "^0.4.4",
"@backstage/test-utils-core": "^0.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
@@ -66,13 +66,11 @@ function createRoutesFromChildren(children: ReactNode): RouteObject[] {
});
}
type BackstageRoutesProps = {
type FlatRoutesProps = {
children: ReactNode;
};
export const BackstageRoutes = (
props: BackstageRoutesProps,
): JSX.Element | null => {
export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
const routes = createRoutesFromChildren(props.children);
return useRoutes(routes);
};
+3 -19
View File
@@ -25,25 +25,15 @@ export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
get path() {
return this.config.path;
return this.config.path ?? '';
}
get title() {
return this.config.title;
}
/**
* This function should not be used, create a separate RouteRef instead
* @deprecated
*/
createSubRoute(): any {
throw new Error(
'This method should not be called, create a separate RouteRef instead',
);
}
toString() {
return `routeRef{path=${this.path}}`;
return `routeRef{title=${this.title}}`;
}
}
@@ -54,13 +44,7 @@ export function createRouteRef<
return new AbsoluteRouteRef<Params>(config);
}
const create = Symbol('create-external-route-ref');
export class ExternalRouteRef {
static [create]() {
return new ExternalRouteRef();
}
private constructor() {}
toString() {
@@ -69,5 +53,5 @@ export class ExternalRouteRef {
}
export function createExternalRouteRef(): ExternalRouteRef {
return ExternalRouteRef[create]();
return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })();
}
+1 -1
View File
@@ -21,6 +21,6 @@ export type {
ConcreteRoute,
MutableRouteRef,
} from './types';
export { BackstageRoutes } from './BackstageRoutes';
export { FlatRoutes } from './FlatRoutes';
export { createRouteRef } from './RouteRef';
export { useRouteRef } from './hooks';
+3 -6
View File
@@ -19,14 +19,10 @@ import { IconComponent } from '../icons';
// @ts-ignore, we're just embedding the Params type for usage in other places
export type RouteRef<Params extends { [param in string]: string } = {}> = {
// TODO(Rugvip): Remove path, look up via registry instead
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
path: string;
icon?: IconComponent;
title: string;
/**
* This function should not be used, create a separate RouteRef instead
* @deprecated
*/
createSubRoute(): any;
};
export type AnyRouteRef = RouteRef<any>;
@@ -51,7 +47,8 @@ export type MutableRouteRef = RouteRef<{}>;
export type RouteRefConfig<Params extends { [param in string]: string }> = {
params?: Array<keyof Params>;
path: string;
/** @deprecated Route refs no longer decide their own path */
path?: string;
icon?: IconComponent;
title: string;
};
+10
View File
@@ -1,5 +1,15 @@
# @backstage/core
## 0.4.3
### Patch Changes
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
- Updated dependencies [a08c32ced]
- Updated dependencies [86c3c652a]
- Updated dependencies [27f2af935]
- @backstage/core-api@0.2.8
## 0.4.2
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.4.2",
"version": "0.4.3",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.2",
"@backstage/core-api": "^0.2.7",
"@backstage/core-api": "^0.2.8",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -65,7 +65,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/cli": "^0.4.4",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+22
View File
@@ -1,5 +1,27 @@
# @backstage/create-app
## 0.3.1
### Patch Changes
- 4e0e3b1bf: Add missing `yarn clean` for app.
For users with existing Backstage installations, add the following under the `scripts` section in `packages/app/package.json`, after the "lint" entry:
```json
"clean": "backstage-cli clean",
```
This will add the missing `yarn clean` for the generated frontend.
- 352a6581f: Added `"start-backend"` script to root `package.json`.
To apply this change to an existing app, add the following script to the root `package.json`:
```json
"start-backend": "yarn workspace backend start"
```
## 0.3.0
### Minor Changes
@@ -24,6 +24,8 @@ const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
require('..');
} else {
// Only used for development, so should be a devDependency
// eslint-disable-next-line import/no-extraneous-dependencies
require('ts-node').register({
transpileOnly: true,
project: path.resolve(__dirname, '../../../tsconfig.json'),
+13 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.3.0",
"version": "0.3.1",
"private": false,
"publishConfig": {
"access": "public"
@@ -39,13 +39,13 @@
"devDependencies": {
"@backstage/backend-common": "^0.4.1",
"@backstage/catalog-model": "^0.6.0",
"@backstage/cli": "^0.4.3",
"@backstage/cli": "^0.4.4",
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.2",
"@backstage/plugin-api-docs": "^0.4.1",
"@backstage/core": "^0.4.3",
"@backstage/plugin-api-docs": "^0.4.2",
"@backstage/plugin-app-backend": "^0.3.3",
"@backstage/plugin-auth-backend": "^0.2.7",
"@backstage/plugin-catalog": "^0.2.8",
"@backstage/plugin-auth-backend": "^0.2.8",
"@backstage/plugin-catalog": "^0.2.9",
"@backstage/plugin-catalog-backend": "^0.5.1",
"@backstage/plugin-catalog-import": "^0.3.2",
"@backstage/plugin-circleci": "^0.2.5",
@@ -58,8 +58,8 @@
"@backstage/plugin-search": "^0.2.4",
"@backstage/plugin-scaffolder-backend": "^0.3.6",
"@backstage/plugin-tech-radar": "^0.3.1",
"@backstage/plugin-techdocs": "^0.5.1",
"@backstage/plugin-techdocs-backend": "^0.5.0",
"@backstage/plugin-techdocs": "^0.5.2",
"@backstage/plugin-techdocs-backend": "^0.5.1",
"@backstage/plugin-user-settings": "^0.2.3",
"@backstage/test-utils": "^0.1.6",
"@backstage/theme": "^0.2.2",
@@ -70,6 +70,11 @@
"@types/recursive-readdir": "^2.2.0",
"ts-node": "^8.6.2"
},
"nodemonConfig": {
"watch": "./src",
"exec": "bin/backstage-create-app",
"ext": "ts"
},
"files": [
"bin",
"dist",
@@ -7,6 +7,7 @@
},
"scripts": {
"start": "yarn workspace app start",
"start-backend": "yarn workspace backend start",
"build": "lerna run build",
"build-image": "yarn workspace backend build-image",
"tsc": "tsc",
@@ -46,6 +46,7 @@
"build": "backstage-cli app:build",
"test": "backstage-cli test",
"lint": "backstage-cli lint",
"clean": "backstage-cli clean",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/dev-utils
## 0.1.7
### Patch Changes
- 696b8ce74: Add new `addPage` method for use with extensions, as well as an `EntityGridItem` to easily create different test cases for entity overview cards.
- Updated dependencies [a08c32ced]
- Updated dependencies [7e0b8cac5]
- Updated dependencies [87c0c53c2]
- @backstage/core@0.4.3
- @backstage/plugin-catalog@0.2.9
## 0.1.6
### Patch Changes
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.6",
"version": "0.1.7",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,7 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.4.0",
"@backstage/core": "^0.4.3",
"@backstage/catalog-model": "^0.6.0",
"@backstage/plugin-catalog": "^0.2.9",
"@backstage/test-utils": "^0.1.5",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -45,7 +47,7 @@
"react-router-dom": "6.0.0-beta.0"
},
"devDependencies": {
"@backstage/cli": "^0.4.1",
"@backstage/cli": "^0.4.4",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React from 'react';
import { Grid, GridProps, makeStyles } from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles<BackstageTheme, { entity: Entity }>(theme => ({
root: ({ entity }) => ({
position: 'relative',
'&::before': {
content: `"${entity.metadata.name}"`,
top: -theme.typography.fontSize + 4,
display: 'block',
position: 'absolute',
color: theme.palette.textSubtle,
},
}),
}));
export const EntityGridItem = ({
entity,
classes,
...rest
}: Omit<GridProps, 'item' | 'container'> & { entity: Entity }): JSX.Element => {
const itemClasses = useStyles({ entity });
return (
<EntityProvider entity={entity}>
<Grid classes={{ root: itemClasses.root, ...classes }} {...rest} item />
</EntityProvider>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export { EntityGridItem } from './EntityGridItem';
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export * from './EntityGridItem';
+47 -5
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { hot } from 'react-hot-loader/root';
import { hot } from 'react-hot-loader';
import React, { ComponentType, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import BookmarkIcon from '@material-ui/icons/Bookmark';
@@ -29,9 +29,25 @@ import {
AlertDisplay,
OAuthRequestDialog,
AnyApiFactory,
IconComponent,
FlatRoutes,
attachComponentData,
} from '@backstage/core';
import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied';
import { Routes } from 'react-router';
import { Outlet } from 'react-router';
const GatheringRoute: (props: {
path: string;
children: JSX.Element;
}) => JSX.Element = () => <Outlet />;
attachComponentData(GatheringRoute, 'core.gatherMountPoints', true);
type RegisterPageOptions = {
element: JSX.Element;
title?: string;
icon?: IconComponent;
};
// TODO(rugvip): export proper plugin type from core that isn't the plugin class
type BackstagePlugin = ReturnType<typeof createPlugin>;
@@ -44,6 +60,8 @@ class DevAppBuilder {
private readonly plugins = new Array<BackstagePlugin>();
private readonly apis = new Array<AnyApiFactory>();
private readonly rootChildren = new Array<ReactNode>();
private readonly routes = new Array<JSX.Element>();
private readonly sidebarItems = new Array<JSX.Element>();
/**
* Register one or more plugins to render in the dev app
@@ -75,6 +93,21 @@ class DevAppBuilder {
return this;
}
addPage({ element, title, icon }: RegisterPageOptions): DevAppBuilder {
const path = `/page-${this.routes.length + 1}`;
this.sidebarItems.push(
<SidebarItem
key={path}
to={path}
text={title ?? path}
icon={icon ?? BookmarkIcon}
/>,
);
this.routes.push(
<GatheringRoute key={path} path={path} children={element} />,
);
return this;
}
/**
* Build a DevApp component using the resources registered so far
*/
@@ -100,7 +133,10 @@ class DevAppBuilder {
<AppRouter>
<SidebarPage>
{sidebar}
<Routes>{deprecatedAppRoutes}</Routes>
<FlatRoutes>
{this.routes}
{deprecatedAppRoutes}
</FlatRoutes>
</SidebarPage>
</AppRouter>
</AppProvider>
@@ -114,7 +150,12 @@ class DevAppBuilder {
* Build and render directory to #root element, with react hot loading.
*/
render(): void {
const DevApp = hot(this.build());
const hotModule =
require.cache['./dev/index.tsx'] ??
require.cache['./dev/index.ts'] ??
module;
const DevApp = hot(hotModule)(this.build());
const paths = this.findPluginPaths(this.plugins);
@@ -166,6 +207,7 @@ class DevAppBuilder {
return (
<Sidebar>
<SidebarSpacer />
{this.sidebarItems}
{sidebarItems}
</Sidebar>
);
@@ -199,7 +241,7 @@ class DevAppBuilder {
// this to provide their own plugin dev wrappers.
/**
* Creates a dev app for rendering one or more plugins and exposing the touchpoints of the plugin.
* Creates a dev app for rendering one or more plugins and exposing the touch points of the plugin.
*/
export function createDevApp() {
return new DevAppBuilder();
+2
View File
@@ -13,4 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './components';
export * from './devApp';
+20
View File
@@ -1,5 +1,25 @@
# @backstage/techdocs-common
## 0.3.1
### Patch Changes
- 8804e8981: Using @backstage/integration package for GitHub/GitLab/Azure tokens and request options.
Most probably you do not have to make any changes in the app because of this change.
However, if you are using the `DirectoryPreparer` or `CommonGitPreparer` exported by
`@backstage/techdocs-common` package, you now need to add pass in a `config` (from `@backstage/config`)
instance as argument.
```
<!-- Before -->
const directoryPreparer = new DirectoryPreparer(logger);
const commonGitPreparer = new CommonGitPreparer(logger);
<!-- Now -->
const directoryPreparer = new DirectoryPreparer(config, logger);
const commonGitPreparer = new CommonGitPreparer(config, logger);
```
## 0.3.0
### Minor Changes
+3 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/techdocs-common",
"description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli",
"version": "0.3.0",
"version": "0.3.1",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -39,6 +39,7 @@
"@backstage/backend-common": "^0.4.1",
"@backstage/catalog-model": "^0.6.0",
"@backstage/config": "^0.1.2",
"@backstage/integration": "^0.1.4",
"@google-cloud/storage": "^5.6.0",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
@@ -50,18 +51,16 @@
"js-yaml": "^3.14.0",
"mime-types": "^2.1.27",
"mock-fs": "^4.13.0",
"nodegit": "^0.27.0",
"recursive-readdir": "^2.2.2",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/cli": "^0.4.4",
"@types/fs-extra": "^9.0.5",
"@types/git-url-parse": "^9.0.0",
"@types/js-yaml": "^3.12.5",
"@types/mime-types": "^2.1.0",
"@types/mock-fs": "^4.13.0",
"@types/nodegit": "^0.26.12",
"@types/recursive-readdir": "^2.2.0"
},
"jest": {
+18 -62
View File
@@ -16,13 +16,17 @@
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Config } from '@backstage/config';
import { getRootLogger, loadBackendConfig } from '@backstage/backend-common';
import {
getAzureHostToken,
getGitHubRequestOptions,
getGitLabRequestOptions,
getAzureRequestOptions,
} from '@backstage/integration';
import {
getGitHost,
getGithubHostToken,
getGitlabHostToken,
getGitRepoType,
getGitHubIntegrationConfig,
getGitLabIntegrationConfig,
getAzureIntegrationConfig,
} from './git-auth';
interface IGitlabBranch {
@@ -86,62 +90,15 @@ function getAzureApiUrl(url: string): URL {
);
}
function getGithubRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
const token = getGithubHostToken(config, host);
if (token) {
headers.Authorization = `token ${token}`;
}
return {
headers,
};
}
function getGitlabRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {
'PRIVATE-TOKEN': '',
};
const token = getGitlabHostToken(config, host);
if (token) {
headers['PRIVATE-TOKEN'] = token;
}
return {
headers,
};
}
function getAzureRequestOptions(config: Config, host: string): RequestInit {
const headers: HeadersInit = {};
const token = getAzureHostToken(config, host);
if (token !== '') {
headers.Authorization = `Basic ${Buffer.from(`:${token}`, 'utf8').toString(
'base64',
)}`;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async function getGithubDefaultBranch(
repositoryUrl: string,
config: Config,
): Promise<string> {
const path = getGithubApiUrl(config, repositoryUrl).toString();
const host = getGitHost(repositoryUrl);
const options = getGithubRequestOptions(config, host);
const integrationConfig = getGitHubIntegrationConfig(config, host);
const options = getGitHubRequestOptions(integrationConfig);
try {
const raw = await fetch(path, options);
@@ -169,9 +126,10 @@ async function getGitlabDefaultBranch(
config: Config,
): Promise<string> {
const path = getGitlabApiUrl(repositoryUrl).toString();
const host = getGitHost(repositoryUrl);
const gitlabHost = getGitHost(repositoryUrl);
const options = getGitlabRequestOptions(config, gitlabHost);
const integrationConfig = getGitLabIntegrationConfig(config, host);
const options = getGitLabRequestOptions(integrationConfig);
try {
const raw = await fetch(path, options);
@@ -203,7 +161,9 @@ async function getAzureDefaultBranch(
): Promise<string> {
const path = getAzureApiUrl(repositoryUrl).toString();
const host = getGitHost(repositoryUrl);
const options = getAzureRequestOptions(config, host);
const integrationConfig = getAzureIntegrationConfig(config, host);
const options = getAzureRequestOptions(integrationConfig);
try {
const urlResponse = await fetch(path, options);
@@ -235,12 +195,8 @@ async function getAzureDefaultBranch(
export const getDefaultBranch = async (
repositoryUrl: string,
config: Config,
): Promise<string> => {
// TODO(Rugvip): Config should not be loaded here, pass it in instead
const config = await loadBackendConfig({
logger: getRootLogger(),
argv: process.argv,
});
const type = getGitRepoType(repositoryUrl);
try {
+44 -55
View File
@@ -15,7 +15,14 @@
*/
import parseGitUrl from 'git-url-parse';
import { Config } from '@backstage/config';
import { getRootLogger, loadBackendConfig } from '@backstage/backend-common';
import {
readGitHubIntegrationConfigs,
readGitLabIntegrationConfigs,
readAzureIntegrationConfigs,
GitHubIntegrationConfig,
GitLabIntegrationConfig,
AzureIntegrationConfig,
} from '@backstage/integration';
export function getGitHost(url: string): string {
const { resource } = parseGitUrl(url);
@@ -34,83 +41,65 @@ export function getGitRepoType(url: string): string {
return type;
}
export function getGithubHostToken(
export const getGitHubIntegrationConfig = (
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.github') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
): GitHubIntegrationConfig => {
const allGitHubConfigs = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken') ??
process.env.GITHUB_TOKEN;
const gitHubIntegrationConfig = allGitHubConfigs.find(v => v.host === host);
if (!gitHubIntegrationConfig) {
throw new Error(`Unable to locate GitHub integration for the host ${host}`);
}
return gitHubIntegrationConfig;
};
return token;
}
export function getGitlabHostToken(
export const getGitLabIntegrationConfig = (
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.gitlab') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
): GitLabIntegrationConfig => {
const allGitLabConfigs = readGitLabIntegrationConfigs(
config.getOptionalConfigArray('integrations.gitlab') ?? [],
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.gitlab.privateToken') ??
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
process.env.GITLAB_TOKEN;
const gitLabIntegrationConfig = allGitLabConfigs.find(v => v.host === host);
if (!gitLabIntegrationConfig) {
throw new Error(`Unable to locate GitLab integration for the host ${host}`);
}
return gitLabIntegrationConfig;
};
return token;
}
export function getAzureHostToken(
export const getAzureIntegrationConfig = (
config: Config,
host: string,
): string | undefined {
const providerConfigs =
config.getOptionalConfigArray('integrations.azure') ?? [];
const hostConfig = providerConfigs.filter(
providerConfig => providerConfig.getOptionalString('host') === host,
): AzureIntegrationConfig => {
const allAzureIntegrationConfig = readAzureIntegrationConfigs(
config.getOptionalConfigArray('integrations.azure') ?? [],
);
const token =
hostConfig[0]?.getOptionalString('token') ??
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
process.env.AZURE_TOKEN;
return token;
}
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> => {
// TODO(Rugvip): Config should not be loaded here, pass it in instead
const config = await loadBackendConfig({
logger: getRootLogger(),
argv: process.argv,
});
const host = getGitHost(repositoryUrl);
const type = getGitRepoType(repositoryUrl);
try {
switch (type) {
case 'github':
return getGithubHostToken(config, host);
return getGitHubIntegrationConfig(config, host).token;
case 'gitlab':
return getGitlabHostToken(config, host);
return getGitLabIntegrationConfig(config, host).token;
case 'azure/api':
return getAzureHostToken(config, host);
return getAzureIntegrationConfig(config, host).token;
default:
throw new Error('Failed to get repository type');
}
+72 -39
View File
@@ -17,19 +17,15 @@
import os from 'os';
import path from 'path';
import parseGitUrl from 'git-url-parse';
import NodeGit, { Clone, Repository } from 'nodegit';
import fs from 'fs-extra';
import { InputError, UrlReader, Git } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { getDefaultBranch } from './default-branch';
import { getGitRepoType, getTokenForGitRepo } from './git-auth';
import { Entity } from '@backstage/catalog-model';
import { InputError, UrlReader } from '@backstage/backend-common';
import { RemoteProtocol } from './stages/prepare/types';
import { Logger } from 'winston';
// Enables core.longpaths on windows to prevent crashing when checking out repos with long foldernames and/or deep nesting
// @ts-ignore
NodeGit.Libgit2.opts(28, 1);
export type ParsedLocationAnnotation = {
type: RemoteProtocol;
target: string;
@@ -94,6 +90,7 @@ export const getLocationForEntity = (
export const getGitRepositoryTempFolder = async (
repositoryUrl: string,
config: Config,
): Promise<string> => {
const parsedGitLocation = parseGitUrl(repositoryUrl);
// removes .git from git location path
@@ -102,6 +99,7 @@ export const getGitRepositoryTempFolder = async (
if (!parsedGitLocation.ref) {
parsedGitLocation.ref = await getDefaultBranch(
parsedGitLocation.toString('https'),
config,
);
}
@@ -118,23 +116,68 @@ export const getGitRepositoryTempFolder = async (
export const checkoutGitRepository = async (
repoUrl: string,
config: Config,
logger: Logger,
): Promise<string> => {
const parsedGitLocation = parseGitUrl(repoUrl);
const repositoryTmpPath = await getGitRepositoryTempFolder(repoUrl);
const token = await getTokenForGitRepo(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: token,
password: 'x-oauth-basic',
logger,
});
parsedGitLocation.token = `${token}:x-oauth-basic`;
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 repository = await Repository.open(repositoryTmpPath);
const currentBranchName = (
await repository.getCurrentBranch()
).shorthand();
await repository.fetch('origin');
await repository.mergeBranches(
currentBranchName,
`origin/${currentBranchName}`,
);
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(
@@ -144,40 +187,30 @@ export const checkoutGitRepository = async (
}
}
if (token) {
const type = getGitRepoType(repoUrl);
switch (type) {
case 'gitlab':
// Personal Access Token
parsedGitLocation.token = `dummyUsername:${token}`;
parsedGitLocation.git_suffix = true;
break;
case 'github':
parsedGitLocation.token = `${token}:x-oauth-basic`;
break;
default:
parsedGitLocation.token = `:${token}`;
}
}
const repositoryCheckoutUrl = parsedGitLocation.toString('https');
fs.mkdirSync(repositoryTmpPath, { recursive: true });
await Clone.clone(repositoryCheckoutUrl, repositoryTmpPath);
await git.clone({ url: repositoryCheckoutUrl, dir: repositoryTmpPath });
return repositoryTmpPath;
};
export const getLastCommitTimestamp = async (
repositoryUrl: string,
config: Config,
logger: Logger,
): Promise<number> => {
const repositoryLocation = await checkoutGitRepository(repositoryUrl, logger);
const repositoryLocation = await checkoutGitRepository(
repositoryUrl,
config,
logger,
);
const repository = await Repository.open(repositoryLocation);
const commit = await repository.getReferenceCommit('HEAD');
const git = Git.fromAuth({ logger });
const sha = await git.resolveRef({ dir: repositoryLocation, ref: 'HEAD' });
const commit = await git.readCommit({ dir: repositoryLocation, sha });
return commit.date().getTime();
return commit.commit.committer.timestamp;
};
export const getDocFilesFromRepository = async (
@@ -15,6 +15,7 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { CommonGitPreparer } from './commonGit';
import { checkoutGitRepository } from '../../helpers';
@@ -43,11 +44,13 @@ const createMockEntity = (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(logger);
const preparer = new CommonGitPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/techdocs-ref':
@@ -62,7 +65,7 @@ describe('commonGit preparer', () => {
});
it('should prepare temp docs path from gitlab repo', async () => {
const preparer = new CommonGitPreparer(logger);
const preparer = new CommonGitPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/techdocs-ref':
@@ -77,7 +80,7 @@ describe('commonGit preparer', () => {
});
it('should prepare temp docs path from azure repo', async () => {
const preparer = new CommonGitPreparer(logger);
const preparer = new CommonGitPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/techdocs-ref':
@@ -14,17 +14,20 @@
* limitations under the License.
*/
import path from 'path';
import { Entity } from '@backstage/catalog-model';
import { PreparerBase } from './types';
import parseGitUrl from 'git-url-parse';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { PreparerBase } from './types';
import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers';
import { Logger } from 'winston';
export class CommonGitPreparer implements PreparerBase {
private readonly config: Config;
private readonly logger: Logger;
constructor(logger: Logger) {
constructor(config: Config, logger: Logger) {
this.config = config;
this.logger = logger;
}
@@ -35,7 +38,11 @@ export class CommonGitPreparer implements PreparerBase {
);
try {
const repoPath = await checkoutGitRepository(target, this.logger);
const repoPath = await checkoutGitRepository(
target,
this.config,
this.logger,
);
const parsedGitLocation = parseGitUrl(target);
return path.join(repoPath, parsedGitLocation.filepath);
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DirectoryPreparer } from './dir';
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { DirectoryPreparer } from './dir';
import { checkoutGitRepository } from '../../helpers';
function normalizePath(path: string) {
@@ -44,9 +45,11 @@ const createMockEntity = (annotations: {}) => {
};
};
const mockConfig = new ConfigReader({});
describe('directory preparer', () => {
it('should merge managed-by-location and techdocs-ref when techdocs-ref is relative', async () => {
const directoryPreparer = new DirectoryPreparer(logger);
const directoryPreparer = new DirectoryPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/managed-by-location':
@@ -60,7 +63,7 @@ describe('directory preparer', () => {
});
it('should merge managed-by-location and techdocs-ref when techdocs-ref is absolute', async () => {
const directoryPreparer = new DirectoryPreparer(logger);
const directoryPreparer = new DirectoryPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/managed-by-location':
@@ -74,7 +77,7 @@ describe('directory preparer', () => {
});
it('should merge managed-by-location and techdocs-ref when managed-by-location is a git repository', async () => {
const directoryPreparer = new DirectoryPreparer(logger);
const directoryPreparer = new DirectoryPreparer(mockConfig, logger);
const mockEntity = createMockEntity({
'backstage.io/managed-by-location':
@@ -15,6 +15,7 @@
*/
import { PreparerBase } from './types';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import path from 'path';
import { parseReferenceAnnotation, checkoutGitRepository } from '../../helpers';
import { InputError } from '@backstage/backend-common';
@@ -22,9 +23,11 @@ import parseGitUrl from 'git-url-parse';
import { Logger } from 'winston';
export class DirectoryPreparer implements PreparerBase {
private readonly config: Config;
private readonly logger: Logger;
constructor(logger: Logger) {
constructor(config: Config, logger: Logger) {
this.config = config;
this.logger = logger;
}
@@ -43,7 +46,11 @@ export class DirectoryPreparer implements PreparerBase {
case 'url':
case 'azure/api': {
const parsedGitLocation = parseGitUrl(target);
const repoLocation = await checkoutGitRepository(target, this.logger);
const repoLocation = await checkoutGitRepository(
target,
this.config,
this.logger,
);
return path.dirname(
path.join(repoLocation, parsedGitLocation.filepath),
@@ -30,18 +30,15 @@ export class Preparers implements PreparerBuilder {
private preparerMap = new Map<RemoteProtocol, PreparerBase>();
static async fromConfig(
// @ts-ignore
// Config not used now, but will be used in urlPreparer when it starts using
// @backstage/integration to get the tokens for providers.
config: Config,
{ logger, reader }: factoryOptions,
): Promise<PreparerBuilder> {
const preparers = new Preparers();
const directoryPreparer = new DirectoryPreparer(logger);
const directoryPreparer = new DirectoryPreparer(config, logger);
preparers.register('dir', directoryPreparer);
const commonGitPreparer = new CommonGitPreparer(logger);
const commonGitPreparer = new CommonGitPreparer(config, logger);
preparers.register('github', commonGitPreparer);
preparers.register('gitlab', commonGitPreparer);
preparers.register('azure/api', commonGitPreparer);
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import { Entity } from '@backstage/catalog-model';
import { UrlReader } from '@backstage/backend-common';
import { PreparerBase } from './types';
import { getDocFilesFromRepository } from '../../helpers';
import { Logger } from 'winston';
import { UrlReader } from '@backstage/backend-common';
export class UrlPreparer implements PreparerBase {
private readonly logger: Logger;
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import mockFs from 'mock-fs';
import * as os from 'os';
import * as path from 'path';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
describe('getHeadersForFileExtension', () => {
@@ -39,9 +41,11 @@ describe('getHeadersForFileExtension', () => {
});
describe('getFileTreeRecursively', () => {
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
beforeEach(() => {
mockFs({
'/rootDir': {
[root]: {
file1: '',
subDirA: {
file2: '',
@@ -57,9 +61,9 @@ describe('getFileTreeRecursively', () => {
});
it('returns complete file tree of a path', async () => {
const fileList = await getFileTreeRecursively('/rootDir');
const fileList = await getFileTreeRecursively(root);
expect(fileList.length).toBe(2);
expect(fileList).toContain('/rootDir/file1');
expect(fileList).toContain('/rootDir/subDirA/file2');
expect(fileList).toContain(path.resolve(root, 'file1'));
expect(fileList).toContain(path.resolve(root, 'subDirA/file2'));
});
});
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-api-docs
## 0.4.2
### Patch Changes
- 9161531b2: Link register API to catalog-import plugin
- Updated dependencies [a08c32ced]
- Updated dependencies [7e0b8cac5]
- Updated dependencies [87c0c53c2]
- @backstage/core@0.4.3
- @backstage/plugin-catalog@0.2.9
## 0.4.1
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.4.1",
"version": "0.4.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,8 +30,8 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.9",
"@backstage/theme": "^0.2.2",
"@kyma-project/asyncapi-react": "^0.14.2",
"@material-icons/font": "^1.0.2",
@@ -49,8 +49,8 @@
"swagger-ui-react": "^3.37.2"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+6
View File
@@ -1,5 +1,11 @@
# @backstage/plugin-auth-backend
## 0.2.8
### Patch Changes
- cc046682e: fix bug in token expiration date
## 0.2.7
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.2.7",
"version": "0.2.8",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -64,7 +64,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.4.2",
"@backstage/cli": "^0.4.4",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express-session": "^1.17.2",
@@ -56,7 +56,9 @@ export const postMessageResponse = (
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
(window.opener || window.parent).postMessage(originInfo, '*');
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
window.close();
setTimeout(() => {
window.close();
}, 100); // same as the interval of the core-api lib/loginPopup.ts (to address race conditions)
`;
const hash = crypto.createHash('sha256').update(script).digest('base64');
+3 -3
View File
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/plugin-catalog-backend": "^0.5.0",
"@backstage/integration": "^0.1.4",
@@ -50,8 +50,8 @@
"yaml": "^1.10.0"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+16
View File
@@ -1,5 +1,21 @@
# @backstage/plugin-catalog
## 0.2.9
### Patch Changes
- 7e0b8cac5: Add `CatalogIndexPage` and `CatalogEntityPage`, two new extensions that replace the existing `Router` component.
Add `EntityLayout` to replace `EntityPageLayout`, using children instead of an element property, and allowing for collection of all `RouteRef` mount points used within tabs.
Add `EntitySwitch` to be used to select components based on entity data, along with accompanying `isKind`, `isNamespace`, and `isComponentType` filters.
- 87c0c53c2: Add new `EntityProvider` component, which can be used to provide an entity for the `useEntity` hook.
- Updated dependencies [a08c32ced]
- Updated dependencies [359f9d2d8]
- @backstage/core@0.4.3
- @backstage/plugin-techdocs@0.5.2
## 0.2.8
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.2.8",
"version": "0.2.9",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,9 +32,9 @@
"dependencies": {
"@backstage/catalog-client": "^0.3.4",
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-scaffolder": "^0.3.5",
"@backstage/plugin-techdocs": "^0.5.1",
"@backstage/plugin-techdocs": "^0.5.2",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -52,8 +52,8 @@
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@microsoft/microsoft-graph-types": "^1.25.0",
"@testing-library/jest-dom": "^5.10.1",
@@ -16,12 +16,12 @@
import React from 'react';
import { Outlet } from 'react-router';
import { EntityProvider } from '../EntityProvider';
import { EntityLoaderProvider } from '../EntityLoaderProvider';
export const CatalogEntityPage = () => {
return (
<EntityProvider>
<EntityLoaderProvider>
<Outlet />
</EntityProvider>
</EntityLoaderProvider>
);
};
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React, { ReactNode } from 'react';
import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity';
export const EntityLoaderProvider = ({ children }: { children: ReactNode }) => {
const { entity, loading, error } = useEntityFromUrl();
return (
<EntityContext.Provider value={{ entity, loading, error }}>
{children}
</EntityContext.Provider>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export { EntityLoaderProvider } from './EntityLoaderProvider';
@@ -13,15 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import React, { ReactNode } from 'react';
import { useEntityFromUrl, EntityContext } from '../../hooks/useEntity';
import { EntityContext } from '../../hooks/useEntity';
export const EntityProvider = ({ children }: { children: ReactNode }) => {
const { entity, loading, error } = useEntityFromUrl();
return (
<EntityContext.Provider value={{ entity, loading, error }}>
{children}
</EntityContext.Provider>
);
type EntityProviderProps = {
entity: Entity;
children: ReactNode;
};
export const EntityProvider = ({ entity, children }: EntityProviderProps) => (
<EntityContext.Provider
value={{
entity,
loading: Boolean(entity),
error: undefined,
}}
>
{children}
</EntityContext.Provider>
);
+3 -3
View File
@@ -23,7 +23,7 @@ import { entityRoute, rootRoute } from '../routes';
import { CatalogPage } from './CatalogPage';
import { EntityNotFound } from './EntityNotFound';
import { EntityPageLayout } from './EntityPageLayout';
import { EntityProvider } from './EntityProvider';
import { EntityLoaderProvider } from './EntityLoaderProvider';
const DefaultEntityPage = () => (
<EntityPageLayout>
@@ -79,9 +79,9 @@ export const Router = ({
<Route
path={`${entityRoute.path}`}
element={
<EntityProvider>
<EntityLoaderProvider>
<EntityPageSwitch EntityPage={EntityPage} />
</EntityProvider>
</EntityLoaderProvider>
}
/>
<Route
+1 -1
View File
@@ -55,7 +55,7 @@ export const useEntityFromUrl = (): EntityLoadingStatus => {
};
/**
* Always going to return an entity, or throw an error if not a descendant of a EntityProvider.
* Grab Entity from the context and its current loading state.
*/
export const useEntity = () => {
const { entity, loading, error } = useContext<{
+1
View File
@@ -18,6 +18,7 @@ export * from '@backstage/catalog-client';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
export { EntityLayout } from './components/EntityLayout';
export { EntityProvider } from './components/EntityProvider';
export * from './components/EntitySwitch';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
+3 -3
View File
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -50,8 +50,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -49,8 +49,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-cost-insights
## 0.5.4
### Patch Changes
- 3fca9adb9: Fix links in sample instructions
- Updated dependencies [a08c32ced]
- @backstage/core@0.4.3
## 0.5.3
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-cost-insights",
"version": "0.5.3",
"version": "0.5.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/test-utils": "^0.1.5",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -56,8 +56,8 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
@@ -16,7 +16,7 @@
import React from 'react';
import { Box, Typography } from '@material-ui/core';
import { CodeSnippet } from '@backstage/core';
import { CodeSnippet, Link } from '@backstage/core';
import { AlertInstructionsLayout } from '../AlertInstructionsLayout';
export const LabelDataflowInstructionsPage = () => {
@@ -31,10 +31,10 @@ export const LabelDataflowInstructionsPage = () => {
</Typography>
<Typography paragraph>
In Cloud Dataflow, labels can be added to a job either programmatically
or via the command-line when launching a job. Note that GCP has
<a href="https://cloud.google.com/compute/docs/labeling-resources#restrictions">
or via the command-line when launching a job. Note that GCP has{' '}
<Link to="https://cloud.google.com/compute/docs/labeling-resources#restrictions">
restrictions
</a>{' '}
</Link>{' '}
on the length and characters that can be used in labels.
</Typography>
<Typography paragraph>
@@ -46,9 +46,9 @@ export const LabelDataflowInstructionsPage = () => {
<Typography variant="h3">DataflowPipelineOptions</Typography>
<Typography paragraph>
Dataflow jobs using Beam's{' '}
<a href="https://beam.apache.org/releases/javadoc/2.3.0/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.html">
<Link to="https://beam.apache.org/releases/javadoc/2.3.0/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.html">
DataflowPipelineOptions
</a>{' '}
</Link>{' '}
directly can use the <b>setLabels</b> function to add one or more
labels:
<CodeSnippet
@@ -79,13 +79,13 @@ sc.optionsAs[DataflowPipelineOptions].setLabels(Map("job-id" -> "my-dataflow-job
</Typography>
<Typography paragraph>
For more information on specifying options, see the{' '}
<a href="https://cloud.google.com/dataflow/docs/guides/specifying-exec-params">
<Link to="https://cloud.google.com/dataflow/docs/guides/specifying-exec-params">
Dataflow documentation
</a>{' '}
</Link>{' '}
or{' '}
<a href="https://spotify.github.io/scio/api/com/spotify/scio/ScioContext.html">
<Link to="https://spotify.github.io/scio/api/com/spotify/scio/ScioContext.html">
Scio Scaladoc
</a>
</Link>
.
</Typography>
</Box>
@@ -135,7 +135,7 @@ export const ProjectGrowthInstructionsPage = () => {
<Typography paragraph>
Next, evaluate whether the growth is significant. This helps avoid
premature optimization, where cost in engineering time is more than
would be saved from the optimization over a reasonable timeframe.
would be saved from the optimization over a reasonable time frame.
</Typography>
<Typography paragraph>
We recommend reframing the cost growth itself in terms of engineering
@@ -205,7 +205,7 @@ export const ProjectGrowthInstructionsPage = () => {
Is the workload using cloud resources efficiently? For compute
resources, do the utilization metrics look reasonable? Autoscaling
infrastructure, such as Kubernetes, can run workloads more efficiently
without comprimising reliability.
without compromising reliability.
</Typography>
<Typography variant="h5">Lifecycle</Typography>
<Typography paragraph>
+1 -1
View File
@@ -24,7 +24,7 @@ export const DEFAULT_DURATION = Duration.P30D;
* Derive the start date of a given period, assuming two repeating intervals.
*
* @param duration see comment on Duration enum
* @param endDate from CostInsightsApi.getLastCompleteBillingDate + 1 day
* @param exclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate + 1 day
*/
export function inclusiveStartDateOf(
duration: Duration,
+14 -4
View File
@@ -234,14 +234,24 @@ export function aggregationFor(
'day',
);
function nextDelta(): number {
const varianceFromBaseline = 0.15;
// Let's give positive vibes in trendlines - higher change for positive delta with >0.5 value
const positiveTrendChance = 0.55;
const normalization = positiveTrendChance - 1;
return baseline * (Math.random() + normalization) * varianceFromBaseline;
}
return [...Array(days).keys()].reduce(
(values: DateAggregation[], i: number): DateAggregation[] => {
const last = values.length ? values[values.length - 1].amount : baseline;
const date = dayjs(inclusiveStartDateOf(duration, endDate))
.add(i, 'day')
.format(DEFAULT_DATE_FORMAT);
const amount = Math.max(0, last + nextDelta());
values.push({
date: dayjs(inclusiveStartDateOf(duration, endDate))
.add(i, 'day')
.format(DEFAULT_DATE_FORMAT),
amount: Math.max(0, last + (baseline / 20) * (Math.random() * 2 - 1)),
date: date,
amount: amount,
});
return values;
},
+3 -3
View File
@@ -30,7 +30,7 @@
"start": "backstage-cli plugin:serve"
},
"dependencies": {
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -42,8 +42,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+9
View File
@@ -0,0 +1,9 @@
# @backstage/plugin-fossa
## 0.1.1
### Patch Changes
- 7afdfef98: Bump dependency versions of @backstage/core, cli and test-utils
- Updated dependencies [a08c32ced]
- @backstage/core@0.4.3
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-fossa",
"version": "0.1.0",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -43,8 +43,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -41,8 +41,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -33,7 +33,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/core-api": "^0.2.7",
"@backstage/plugin-catalog": "^0.2.8",
"@backstage/theme": "^0.2.2",
@@ -51,8 +51,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -42,8 +42,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -43,8 +43,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -46,8 +46,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -33,7 +33,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-kubernetes-backend": "^0.2.3",
"@backstage/theme": "^0.2.2",
"@kubernetes/client-node": "^0.12.1",
@@ -46,8 +46,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -33,7 +33,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/core-api": "^0.2.6",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
@@ -47,8 +47,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -31,7 +31,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -41,8 +41,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -21,7 +21,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -33,8 +33,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -31,7 +31,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -44,8 +44,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+11
View File
@@ -1,5 +1,16 @@
# @backstage/plugin-register-component
## 0.2.6
### Patch Changes
- 1517876fd: Register component plugin is deprecated in favor of @backstage/plugin-catalog-import
- Updated dependencies [a08c32ced]
- Updated dependencies [7e0b8cac5]
- Updated dependencies [87c0c53c2]
- @backstage/core@0.4.3
- @backstage/plugin-catalog@0.2.9
## 0.2.5
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-register-component",
"version": "0.2.5",
"version": "0.2.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,8 +31,8 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.9",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -45,8 +45,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -3
View File
@@ -32,7 +32,7 @@
},
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/core": "^0.4.2",
"@backstage/core": "^0.4.3",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -47,8 +47,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@backstage/dev-utils": "^0.1.6",
"@backstage/cli": "^0.4.4",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+3 -4
View File
@@ -42,6 +42,7 @@
"command-exists-promise": "^2.0.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -49,20 +50,18 @@
"git-url-parse": "^11.4.3",
"globby": "^11.0.0",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"jsonschema": "^1.2.6",
"morgan": "^1.10.0",
"nodegit": "0.27.0",
"uuid": "^8.2.0",
"winston": "^3.2.1",
"yaml": "^1.10.0",
"cross-fetch": "^3.0.6"
"yaml": "^1.10.0"
},
"devDependencies": {
"@backstage/cli": "^0.4.3",
"@octokit/types": "^5.4.1",
"@types/fs-extra": "^9.0.1",
"@types/git-url-parse": "^9.0.0",
"@types/nodegit": "0.26.11",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"yaml": "^1.10.0"
@@ -14,11 +14,6 @@
* limitations under the License.
*/
const mocks = {
Clone: { clone: jest.fn() },
CheckoutOptions: jest.fn(() => {}),
};
jest.doMock('nodegit', () => mocks);
jest.doMock('fs-extra', () => ({
promises: {
mkdtemp: jest.fn(dir => `${dir}-static`),
@@ -30,10 +25,16 @@ import {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger, Git } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
describe('AzurePreparer', () => {
const mockGitClient = {
clone: jest.fn(),
};
jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any);
let mockEntity: TemplateEntityV1alpha1;
beforeEach(() => {
jest.clearAllMocks();
@@ -77,18 +78,7 @@ describe('AzurePreparer', () => {
};
});
it('calls the clone command with the correct arguments for a repository', async () => {
const preparer = new AzurePreparer(new ConfigReader({}));
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
expect.any(String),
{},
);
});
it('calls the clone command with the correct arguments if an access token is provided for a repository', async () => {
it('initializes git client with the correct arguments if an access token is provided for a repository', async () => {
const preparer = new AzurePreparer(
new ConfigReader({
scaffolder: {
@@ -100,36 +90,44 @@ describe('AzurePreparer', () => {
},
}),
);
const logger = getVoidLogger();
await preparer.prepare(mockEntity, { logger });
expect(Git.fromAuth).toHaveBeenCalledWith({
username: 'notempty',
password: 'fake-token',
logger,
});
});
it('calls the clone command with the correct arguments for a repository', async () => {
const preparer = new AzurePreparer(new ConfigReader({}));
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
expect.any(String),
{
fetchOpts: {
callbacks: {
credentials: expect.anything(),
},
},
},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url:
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
dir: expect.any(String),
});
});
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
const preparer = new AzurePreparer(new ConfigReader({}));
delete mockEntity.spec.path;
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
expect.any(String),
{},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url:
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
dir: expect.any(String),
});
});
it('return the temp directory with the path to the folder if it is specified', async () => {
const preparer = new AzurePreparer(new ConfigReader({}));
mockEntity.spec.path = './template/test/1/2/3';
const response = await preparer.prepare(mockEntity, {
logger: getVoidLogger(),
});
@@ -142,6 +140,7 @@ describe('AzurePreparer', () => {
it('return the working directory with the path to the folder if it is specified', async () => {
const preparer = new AzurePreparer(new ConfigReader({}));
mockEntity.spec.path = './template/test/1/2/3';
const response = await preparer.prepare(mockEntity, {
logger: getVoidLogger(),
workingDirectory: '/workDir',
@@ -18,10 +18,9 @@ import fs from 'fs-extra';
import path from 'path';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { InputError, Git } from '@backstage/backend-common';
import { PreparerBase, PreparerOptions } from './types';
import GitUriParser from 'git-url-parse';
import { Clone, Cred } from 'nodegit';
import { Config } from '@backstage/config';
export class AzurePreparer implements PreparerBase {
@@ -38,6 +37,7 @@ export class AzurePreparer implements PreparerBase {
): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
const { logger } = opts;
if (!['azure/api', 'url'].includes(protocol)) {
throw new InputError(
@@ -57,19 +57,20 @@ export class AzurePreparer implements PreparerBase {
template.spec.path ?? '.',
);
const options = this.privateToken
? {
fetchOpts: {
callbacks: {
credentials: () =>
// Username can anything but the empty string according to: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat
Cred.userpassPlaintextNew('notempty', this.privateToken),
},
},
}
: {};
// Username can be anything but the empty string according to:
// https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page#use-a-pat
const git = this.privateToken
? Git.fromAuth({
password: this.privateToken,
username: 'notempty',
logger,
})
: Git.fromAuth({ logger });
await Clone.clone(repositoryCheckoutUrl, tempDir, options);
await git.clone({
url: repositoryCheckoutUrl,
dir: tempDir,
});
return path.resolve(tempDir, templateDirectory);
}
@@ -14,11 +14,6 @@
* limitations under the License.
*/
const mocks = {
Clone: { clone: jest.fn() },
CheckoutOptions: jest.fn(() => {}),
};
jest.doMock('nodegit', () => mocks);
jest.doMock('fs-extra', () => ({
promises: {
mkdtemp: jest.fn(dir => `${dir}-static`),
@@ -30,10 +25,16 @@ import {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger, Git } from '@backstage/backend-common';
describe('GitHubPreparer', () => {
let mockEntity: TemplateEntityV1alpha1;
const mockGitClient = {
clone: jest.fn(),
};
jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any);
beforeEach(() => {
jest.clearAllMocks();
mockEntity = {
@@ -77,28 +78,24 @@ describe('GitHubPreparer', () => {
});
it('calls the clone command with the correct arguments for a repository', async () => {
const preparer = new GithubPreparer();
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://github.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
checkoutBranch: 'master',
},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://github.com/benjdlambert/backstage-graphql-template',
dir: expect.any(String),
});
});
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
const preparer = new GithubPreparer();
delete mockEntity.spec.path;
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://github.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
checkoutBranch: 'master',
},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://github.com/benjdlambert/backstage-graphql-template',
dir: expect.any(String),
});
});
it('return the temp directory with the path to the folder if it is specified', async () => {
@@ -128,19 +125,14 @@ describe('GitHubPreparer', () => {
it('calls the clone command with the token when provided', async () => {
const preparer = new GithubPreparer({ token: 'abc' });
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://github.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
checkoutBranch: 'master',
fetchOpts: {
callbacks: {
credentials: expect.any(Function),
},
},
},
);
const logger = getVoidLogger();
await preparer.prepare(mockEntity, { logger });
expect(Git.fromAuth).toHaveBeenCalledWith({
logger,
username: 'abc',
password: 'x-oauth-basic',
});
});
});
@@ -18,10 +18,9 @@ import fs from 'fs-extra';
import path from 'path';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { InputError, Git } from '@backstage/backend-common';
import { PreparerBase, PreparerOptions } from './types';
import GitUriParser from 'git-url-parse';
import { Clone, CloneOptions, Cred } from 'nodegit';
export class GithubPreparer implements PreparerBase {
token?: string;
@@ -36,7 +35,7 @@ export class GithubPreparer implements PreparerBase {
): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
const { token } = this;
const { logger } = opts;
if (!['github', 'url'].includes(protocol)) {
throw new InputError(
@@ -56,25 +55,21 @@ export class GithubPreparer implements PreparerBase {
template.spec.path ?? '.',
);
let cloneOptions: CloneOptions = {
checkoutBranch: parsedGitLocation.ref,
};
const checkoutLocation = path.resolve(tempDir, templateDirectory);
if (token) {
cloneOptions = {
...cloneOptions,
fetchOpts: {
callbacks: {
credentials() {
return Cred.userpassPlaintextNew(token, 'x-oauth-basic');
},
},
},
};
}
const git = this.token
? Git.fromAuth({
username: this.token,
password: 'x-oauth-basic',
logger,
})
: Git.fromAuth({ logger });
await Clone.clone(repositoryCheckoutUrl, tempDir, cloneOptions);
await git.clone({
url: repositoryCheckoutUrl,
dir: tempDir,
});
return path.resolve(tempDir, templateDirectory);
return checkoutLocation;
}
}
@@ -13,11 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const mocks = {
Clone: { clone: jest.fn() },
CheckoutOptions: jest.fn(() => {}),
};
jest.doMock('nodegit', () => mocks);
jest.doMock('fs-extra', () => ({
promises: {
mkdtemp: jest.fn(dir => `${dir}-static`),
@@ -30,7 +25,7 @@ import {
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger, Git } from '@backstage/backend-common';
const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({
apiVersion: 'backstage.io/v1alpha1',
@@ -72,6 +67,12 @@ const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({
describe('GitLabPreparer', () => {
let mockEntity: TemplateEntityV1alpha1;
const mockGitClient = {
clone: jest.fn(),
};
jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any);
beforeEach(() => {
jest.clearAllMocks();
});
@@ -80,13 +81,13 @@ describe('GitLabPreparer', () => {
it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(new ConfigReader({}));
mockEntity = mockEntityWithProtocol(protocol);
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template',
dir: expect.any(String),
});
});
it(`calls the clone command with the correct arguments if an access token is provided in integrations for a repository using the ${protocol} protocol`, async () => {
@@ -103,19 +104,15 @@ describe('GitLabPreparer', () => {
}),
);
mockEntity = mockEntityWithProtocol(protocol);
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
fetchOpts: {
callbacks: {
credentials: expect.anything(),
},
},
},
);
const logger = getVoidLogger();
await preparer.prepare(mockEntity, { logger });
expect(Git.fromAuth).toHaveBeenCalledWith({
logger,
username: 'oauth2',
password: 'fake-token',
});
});
it(`calls the clone command with the correct arguments if an access token is provided in scaffolder for a repository using the ${protocol} protocol`, async () => {
@@ -127,32 +124,28 @@ describe('GitLabPreparer', () => {
}),
);
mockEntity = mockEntityWithProtocol(protocol);
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{
fetchOpts: {
callbacks: {
credentials: expect.anything(),
},
},
},
);
const logger = getVoidLogger();
await preparer.prepare(mockEntity, { logger });
expect(Git.fromAuth).toHaveBeenCalledWith({
logger,
username: 'oauth2',
password: 'fake-token',
});
});
it(`calls the clone command with the correct arguments for a repository when no path is provided using the ${protocol} protocol`, async () => {
const preparer = new GitlabPreparer(new ConfigReader({}));
mockEntity = mockEntityWithProtocol(protocol);
delete mockEntity.spec.path;
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mocks.Clone.clone).toHaveBeenNthCalledWith(
1,
'https://gitlab.com/benjdlambert/backstage-graphql-template',
expect.any(String),
{},
);
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template',
dir: expect.any(String),
});
});
it(`return the temp directory with the path to the folder if it is specified using the ${protocol} protocol`, async () => {
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InputError } from '@backstage/backend-common';
import { InputError, Git } from '@backstage/backend-common';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
@@ -22,7 +22,6 @@ import {
} from '@backstage/integration';
import fs from 'fs-extra';
import GitUriParser from 'git-url-parse';
import { Clone, Cred } from 'nodegit';
import os from 'os';
import path from 'path';
import { parseLocationAnnotation } from '../helpers';
@@ -46,6 +45,7 @@ export class GitlabPreparer implements PreparerBase {
opts: PreparerOptions,
): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
const { logger } = opts;
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) {
@@ -67,17 +67,18 @@ export class GitlabPreparer implements PreparerBase {
);
const token = this.getToken(parsedGitLocation.resource);
const options = token
? {
fetchOpts: {
callbacks: {
credentials: () => Cred.userpassPlaintextNew('oauth2', token),
},
},
}
: {};
const git = token
? Git.fromAuth({
password: token,
username: 'oauth2',
logger,
})
: Git.fromAuth({ logger });
await Clone.clone(repositoryCheckoutUrl, tempDir, options);
await git.clone({
url: repositoryCheckoutUrl,
dir: tempDir,
});
return path.resolve(tempDir, templateDirectory);
}
@@ -13,17 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
jest.mock('nodegit');
jest.mock('azure-devops-node-api/GitApi');
jest.mock('azure-devops-node-api/interfaces/GitInterfaces');
jest.mock('./helpers', () => ({
pushToRemoteUserPass: jest.fn(),
}));
jest.mock('./helpers');
import { AzurePublisher } from './azure';
import { GitApi } from 'azure-devops-node-api/GitApi';
import { pushToRemoteUserPass } from './helpers';
import * as helpers from './helpers';
import { getVoidLogger } from '@backstage/backend-common';
const { mockGitApi } = require('azure-devops-node-api/GitApi') as {
mockGitApi: {
@@ -33,6 +28,7 @@ const { mockGitApi } = require('azure-devops-node-api/GitApi') as {
describe('Azure Publisher', () => {
const publisher = new AzurePublisher(new GitApi('', []), 'fake-token');
const logger = getVoidLogger();
beforeEach(() => {
jest.clearAllMocks();
@@ -50,6 +46,7 @@ describe('Azure Publisher', () => {
owner: 'bob',
},
directory: '/tmp/test',
logger,
});
expect(result).toEqual({
@@ -63,12 +60,12 @@ describe('Azure Publisher', () => {
},
'project',
);
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
'/tmp/test',
'https://dev.azure.com/organization/project/_git/repo',
'notempty',
'fake-token',
);
expect(helpers.initRepoAndPush).toHaveBeenCalledWith({
dir: '/tmp/test',
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
auth: { username: 'notempty', password: 'fake-token' },
logger,
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More