Merge branch 'master' of github.com:spotify/backstage into add-microsoft-azure-auth

This commit is contained in:
Raghunandan Balachandran
2020-08-25 17:15:51 +02:00
151 changed files with 2067 additions and 832 deletions
+31 -1
View File
@@ -14,7 +14,37 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Required to retrieve git history
- run: yarn install && yarn build-storybook
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn build-storybook
- uses: chromaui/action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -1,11 +1,11 @@
name: Frontend CI
name: CI
on:
pull_request:
paths-ignore:
- 'microsite/**'
jobs:
build:
verify:
runs-on: ubuntu-latest
strategy:
@@ -20,34 +20,52 @@ jobs:
- uses: actions/checkout@v2
- name: fetch branch master
run: git fetch origin master
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows.
# TODO(Rugvip): move this to composite action once all features we use are supported
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
# Cache every node_modules folder inside the monorepo
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
# We use both yarn.lock and package.json as cache keys to ensure that
# changes to local monorepo packages bust the cache.
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
# If we get a cache hit for node_modules, there's no need to bring in the global
# yarn cache or run yarn install, as all dependencies will be installed already.
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
if: steps.cache-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
# End of yarn setup
- name: check for yarn.lock changes
id: yarn-lock
run: git diff --quiet origin/master HEAD -- yarn.lock
continue-on-error: true
- name: yarn install
run: yarn install --frozen-lockfile
- name: verify doc links
run: node docs/verify-links.js
@@ -59,8 +77,7 @@ jobs:
- name: build changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
# Need to build all dependencies as well to be able to run tests later
run: yarn lerna -- run build --since origin/master --include-dependencies
run: yarn lerna -- run build --since origin/master
- name: build all packages
if: ${{ steps.yarn-lock.outcome == 'failure' }}
@@ -79,6 +96,3 @@ jobs:
- name: verify plugin template
run: yarn lerna -- run diff -- --check
- name: verify storybook
run: yarn workspace storybook build-storybook
@@ -1,12 +1,13 @@
name: CLI Test Windows
name: E2E Test Windows
# Building on windows is really slow, so this workflow is separate from cli.yml and only builds on changes
# Building on windows is really slow, so this workflow is separate from e2e.yml and only builds on changes
# to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock.
on:
pull_request:
paths:
- '.github/workflows/cli-win.yml'
- '.github/workflows/e2e-win.yml'
- 'packages/cli/**'
- 'packages/e2e/**'
- 'packages/create-app/**'
jobs:
@@ -25,23 +26,37 @@ jobs:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn tsc
- run: yarn build
- name: verify app and plugin creation
run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
- name: run E2E test
run: yarn workspace e2e-test start
@@ -1,11 +1,10 @@
name: CLI Test
name: E2E Test Linux
on:
pull_request:
paths-ignore:
- 'microsite/**'
jobs:
build:
runs-on: ${{ matrix.os }}
@@ -34,30 +33,44 @@ jobs:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- run: yarn tsc
- run: yarn build
- name: verify app and plugin creation
- name: yarn build
run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli --ignore backstage-microsite
- name: run E2E test
run: |
sudo sysctl fs.inotify.max_user_watches=524288
yarn workspace e2e-test start
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
run: |
sudo sysctl fs.inotify.max_user_watches=524288
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
+53
View File
@@ -0,0 +1,53 @@
name: Master Build Windows
on:
push:
branches: [master]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
node-version: [12.x]
env:
CI: true
NODE_OPTIONS: --max-old-space-size=4096
steps:
- uses: actions/checkout@v2
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
# Tests are broken on Windows, disabled for now
# - name: test
# run: yarn lerna -- run test
+22 -17
View File
@@ -1,4 +1,4 @@
name: Master Build
name: Main Master Build
on:
push:
@@ -18,29 +18,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: lint
run: yarn lerna -- run lint
+20 -18
View File
@@ -21,32 +21,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: build microsite
run: yarn workspace backstage-microsite build
@@ -25,32 +25,34 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: find location of global yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: cache node_modules
uses: actions/cache@v2
with:
path: node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: build microsite
run: yarn workspace backstage-microsite build
+14 -13
View File
@@ -1,13 +1,14 @@
| Organization | Contact | Description of Use |
| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| Organization | Contact | Description of Use |
| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling |
+2
View File
@@ -8,6 +8,8 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
> Collect changes for the next release below
## v0.1.1-alpha.19
### @backstage/create-app
- Many plugins have been added to the catalog and will for now be required to be added to separate apps as well. This will be solved as [#1536](https://github.com/spotify/backstage/issues/1536) gets sorted out, but for now you may need to install some plugins just to get pages to work.
-10
View File
@@ -29,16 +29,6 @@ For more information go to [backstage.io](https://backstage.io) or join our [Dis
A detailed project roadmap, including already delivered milestones, is available [here](https://backstage.io/docs/overview/roadmap).
## Overview
The Backstage platform consists of a number of different components:
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/getting-started/create-an-app.md).
- [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_.
- [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.
- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins.
- **identity** - A backend service that holds your organisation's metadata.
## Getting Started
There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do.
+1
View File
@@ -7,6 +7,7 @@ metadata:
annotations:
github.com/project-slug: spotify/backstage
backstage.io/github-actions-id: spotify/backstage
backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
spec:
type: library
owner: Spotify
+1
View File
@@ -12,6 +12,7 @@ better yet, a pull request.
- [Architecture and terminology](overview/architecture-terminology.md)
- [Roadmap](overview/roadmap.md)
- [Vision](overview/vision.md)
- [Strategies for adopting](overview/adopting.md)
- Getting started
- [Running Backstage locally](getting-started/index.md)
- [Installation](getting-started/installation.md)
+11 -6
View File
@@ -71,18 +71,23 @@ import {
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
ConfigApi
} from '@backstage/core';
const builder = ApiRegistry.builder();
// The alert API is a self-contained implementation that shows alerts to the user.
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const apis = (config: ConfigApi) => {
const builder = ApiRegistry.builder();
// The error API uses the alert API to send error notifications to the user.
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
// The alert API is a self-contained implementation that shows alerts to the user.
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
// The error API uses the alert API to send error notifications to the user.
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
return builder.build();
}
const app = createApp({
apis: apiBuilder.build(),
apis,
// ... other config
});
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

+2 -2
View File
@@ -3,7 +3,7 @@ id: design
title: Design
---
![header](../assets/dls/designheader.png)
![header](../assets/dls/designheader-updated.png)
Much like Backstage Open Source, this is a _living_ document! We'll keep this
updated as we evolve our practices!
@@ -116,7 +116,7 @@ components. If youd like to help build up our design system, you can also add
components weve designed to the Storybook as well.
**[Figma](https://www.figma.com/@backstage)** - we're stoked to be using Figma
Community to share our design assets. You can duplicate our component library
Community to share our design assets. You can duplicate our UI Kit
and design your own plugin for Backstage.
**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be
@@ -370,8 +370,8 @@ metadata:
description:
Next.js application skeleton for creating isomorphic web applications.
tags:
- Recommended
- React
- recommended
- react
spec:
owner: web@example.com
templater: cookiecutter
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+71 -58
View File
@@ -3,55 +3,24 @@ id: system-model
title: System Model
---
We believe that a strong shared understanding and terminology around systems,
software and resources leads to a better Backstage experience.
We believe that a strong shared understanding and terminology around software
and resources leads to a better Backstage experience.
_This description originates from
[this RFC](https://github.com/spotify/backstage/issues/390). Note that some of
the concepts are not yet supported in Backstage._
## Concepts
## Core Entities
We model our technology using these five concepts (further explained below):
We model software in the Backstage catalogue using these three core entities
(further explained below):
- **Domains** are a high-level grouping of systems
- **Systems** encapsulate the implementation of APIs
- **APIs** are the boundaries between different components and systems
- **Components** are pieces of software
- **Components** are individual pieces of software
- **APIs** are the boundaries between different components
- **Resources** are physical or virtual infrastructure needed to operate a
system
component
![Software Ecosystem Model_ Public Github version](https://user-images.githubusercontent.com/24575/77633084-39bcde80-6f4f-11ea-8251-f8df561a3652.png)
### Domain
While systems are the basic level of encapsulation for resources, components and
APIs, it is often useful to group a collection of systems that share
terminology, domain models, business purpose, or documentation, i.e. they form a
bounded context.
For example, it would make sense if the different systems in the “Payments”
domain would come with some documentation on how to accept payments for a new
product or use-case, share the same entity types in their APIs, and integrate
well with each other.
### System
With increasing complexity in software, we believe that systems form an
important abstraction level to help us reason about software ecosystems. Systems
are a useful concept in that they allow us to ignore the implementation details
of a certain functionality for consumers, while allowing the owning team to make
changes as they see fit (leading to low coupling).
A system, in this sense, is a collection of resources and components that
exposes one or several APIs. Components and resources in a system are typically
owned by the same team and are expected to co-evolve. As such, systems usually
consist of at most a handful of components.
For example, a playlist management system might encapsulate a backend service to
update playlists, a backend service to query them, and a database to store them.
It could expose an RPC API, a daily snapshots dataset, and an event stream of
playlist updates.
![](system-model-core-entities.png)
### Component
@@ -60,34 +29,78 @@ backend service or data pipeline (list not exhaustive). A component can be
tracked in source control, or use some existing open source or commercial
software.
A component can implement APIs for other components to consume. It might depend
on the resources of the system it belongs to, and APIs from other components or
other systems. All other aspects of the component, e.g. any code dependencies,
must be encapsulated.
A component can implement APIs for other components to consume. In turn it
might depend on APIs implemented by other components, or resources that are
attached to it at runtime.
### API
We believe APIs form an important (maybe the most important) abstraction that
allows large software ecosystems to scale. Thus, APIs are a first class citizen
in the Backstage model and the primary way to discover existing functionality in
APIs form an important (maybe the most important) abstraction that allows large
software ecosystems to scale. Thus, APIs are a first class citizen in the
Backstage model and the primary way to discover existing functionality in
the ecosystem.
APIs are implemented by components and form boundaries between components and
systems. They might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a
data schema (eg Avro, TFRecord, ...), or as code interfaces. In any case, APIs
exposed by components need to be in a known machine-readable format so we can
APIs are implemented by components and form boundaries between components. They
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema
(eg Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
components need to be in a known machine-readable format so we can
build further tooling and analysis on top.
Some APIs might be exposed by the system, making them available for any other
Spotify component to consume. Those public APIs must be documented and humanly
discoverable in Backstage.
APIs have a visibility: they are either public (making them available for any
other component to consume), restricted (only available to a whitelisted set of
consumers), or private (only available within their system). As public APIs are
going to be the primary way interaction between components, Backstage supports
documenting, indexing and searching all APIs so we can browse them as
developers.
### Resource
Resources are the infrastructure a system needs to operate, like BigTable
databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with
components and systems will better allow us to visualize resource footprint, and
create tooling around them.
Resources are the infrastructure a component needs to operate at runtime, like
BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together
with components and systems will better allow us to visualize resource
footprint, and create tooling around them.
## Ecosystem Modeling
A large catalogue of components, APIs and resources can be highly granular
and hard to understand as a whole. It might thus be convenient to further
categorize these entities using the following (optional) concepts:
* **Systems** are a collection of entities that cooperate to perform some
function
* **Domains** relate entities and systems to part of the business
### System
With increasing complexity in software, systems form an important abstraction
level to help us reason about software ecosystems. Systems are a useful concept
in that they allow us to ignore the implementation details of a certain
functionality for consumers, while allowing the owning team to make changes as
they see fit (leading to low coupling).
A system, in this sense, is a collection of resources and components that
exposes one or several public APIs. The main benefit of modelling a system is
that it hides its resources and private APIs between the components for any
consumers. This means that as the owner, you can evolve the implementation, in
terms of components and resources, without your consumers being able to notice.
Typically, a system will consist of at most a handful of components (see
Domain for a grouping of systems).
For example, a playlist management system might encapsulate a backend service
to update playlists, a backend service to query them, and a database to store
them. It could expose an RPC API, a daily snapshots dataset, and an event
stream of playlist updates.
### Domain
While systems are the basic level of encapsulation for related entities, it is
often useful to group a collection of systems that share terminology, domain
models, metrics, KPIs, business purpose, or documentation, i.e. they form a
bounded context.
For example, it would make sense if the different systems in the “Payments”
domain would come with some documentation on how to accept payments for a new
product or use-case, share the same entity types in their APIs, and integrate
well with each other. Other domains could be “Content Ingestion”, “Ads” or
“Search”.
## Current status
@@ -22,8 +22,8 @@ metadata:
Next.js application skeleton for creating isomorphic web applications.
# some tags to display in the frontend
tags:
- Recommended
- React
- recommended
- react
spec:
# which templater key to use in the templaters builder
templater: cookiecutter
@@ -54,7 +54,7 @@ contains more information about the required fields.
Once we have a `template.yaml` ready, we can then add it to the service catalog
for use by the scaffolder.
Currently the catalog supports loading definitions from Github + Local Files. To
Currently the catalog supports loading definitions from GitHub + Local Files. To
load from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
@@ -54,7 +54,7 @@ The `protocol` is set on the
when added to the service catalog. You can see more about this `PreparerKey`
here in [Register your own template](../adding-templates.md)
**note:** Currently the catalog supports loading definitions from Github + Local
**note:** Currently the catalog supports loading definitions from GitHub + Local
Files, which translate into the two `PreparerKeys` `file` and `github`. To load
from other places, not only will there need to be another preparer, but the
support to load the location will also need to be added to the Catalog.
@@ -116,8 +116,8 @@ metadata:
description:
Next.js application skeleton for creating isomorphic web applications.
tags:
- Recommended
- React
- recommended
- react
spec:
owner: web@example.com
templater: handlebars
+1 -1
View File
@@ -34,7 +34,7 @@ internally.
After filling in these variables, you'll get some more fields to fill out which
are required for backstage usage. The owner, which is a `user` in the backstage
system, and the `storePath` which right now must be a Github Organisation and a
system, and the `storePath` which right now must be a GitHub Organisation and a
non-existing github repository name in the format `organisation/reponame`.
![Enter backstage vars](../../assets/software-templates/template-picked-2.png)
@@ -109,7 +109,7 @@ export default async function createPlugin({ logger }: PluginEnvironment) {
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
// Create Github client with your access token from environment variables
// Create GitHub client with your access token from environment variables
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const publisher = new GithubPublisher({ client: githubClient });
+1 -1
View File
@@ -42,7 +42,7 @@ sites with the Backstage UI.
The TechDocs Reader purpose is also to open up the opportunity to integrate
TechDocs widgets for a customized full-featured TechDocs experience.
([coming soon V.2](https://github.com/spotify/backstage/milestone/17))
([coming soon V.3](./README.md#project-roadmap))
[TechDocs Reader](https://github.com/spotify/backstage/blob/master/plugins/techdocs/src/reader/README.md)
@@ -6,26 +6,38 @@ sidebar_label: Creating and Publishing Documentation
This section will guide you through:
- Creating a basic setup for your documentation
- Writing and previewing your documentation in a local Backstage environment
- Creating a build ready for publication
- Publishing your documentation and making your Backstage instance read your
published docs.
- [Create a basic documentation setup](#create-a-basic-documentation-setup)
- [Use the documentation template](#use-the-documentation-template)
- [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository)
- [Writing and previewing your documentation](#writing-and-previewing-your-documentation)
## Prerequisities
- [Docker](https://docs.docker.com/get-docker/)
- Static file hosting
- A working Backstage instance with TechDocs installed (see
[TechDocs getting started](getting-started.md))
## Create a basic documentation setup
In your home directory (also known as `~`), create a directory that contains
your documentation (for example, `hello-docs`). Inside this directory, create a
file called `mkdocs.yml`. Below is a basic example of how it could look.
### Use the documentation template
The `~/hello-docs/mkdocs.yml` file should have the following content:
Your working Backstage instance should by default have a documentation template
added. If not, follow these
[instructions](../software-templates/installation.md#adding-templates) to add
the documentation template.
![Documentation Template](../../assets/techdocs/documentation-template.png)
Create an entity from the documentation template and you will get the needed
setup for free.
### Manually add documentation setup to already existing repository
Prerequisities:
- `catalog-info.yml` file registered to Backstage.
Create a `mkdocs.yml` file in the root of the repository with the following
content:
```yaml
site_name: 'example-docs'
@@ -37,7 +49,20 @@ plugins:
- techdocs-core
```
The `~/hello-docs/docs/index.md` should have the following content:
Update your `catalog-info.yaml` file in the root of the repository with the
following content:
```yaml
metadata:
annotations:
backstage.io/techdocs-ref: dir:./
```
Create a `/docs` folder in the root of the project with at least a `index.md`
file. _(If you add more markdown files, make sure to update the nav in the
mkdocs.yml file to get a proper navigation for your documentation.)_
The `docs/index.md` can for example have the following content:
```md
# example docs
@@ -45,6 +70,9 @@ The `~/hello-docs/docs/index.md` should have the following content:
This is a basic example of documentation.
```
Commit your changes, open a pull request and merge. You will now get your
updated documentation next time you run Backstage!
## Writing and previewing your documentation
Using the `techdocs-cli` you can preview your docs inside a local Backstage
@@ -54,83 +82,6 @@ want to write your documentation.
To do this you can run:
```bash
cd ~/hello-docs/
cd ~/<repository-path>/
npx techdocs-cli serve
```
## Build production ready documentation
To get a build suitable for publication you can build your docs using the
`spotify/techdocs` container:
```bash
cd ~/hello-docs/
docker run -it -w /content -v $(pwd):/content spotify/techdocs build
```
You should now have a folder called `~/hello-docs/site/`.
## Deploy to a file server
In order to serve documentation to TechDocs, our Backstage plugin needs to
download the HTML rendered from the previous step. This will likely exist on an
external file server, or a storage solution such as Google Cloud Storage.
When deploying documentation, it should be deployed on that file server/storage
solution with the following convention: `{id}/{file}`. For example, if you want
to upload the `getting-started/index.html` file for the `backstage`
documentation site, we would upload it to our file server as
`backstage/getting-started/index.html`.
To explain further what this would look like for multiple documentation sites,
take a look at this example file tree that would be represented on your file
server:
```md
/backstage/index.html /backstage/getting-started/index.html
/backstage/contributing/index.html /mkdocs/index.html
/mkdocs/plugin-development/index.html
/mkdocs/plugin-development/debugging/index.html
```
In this file tree, we have two documentation sites available: `backstage` and
`mkdocs`. Each of them expose several pages. Let's say both of these are hosted
on `http://example.com` as the server URL.
When you configure the TechDocs plugin in Backstage to use `http://example.com`
as the file server/storage solution, it will translate the following URLs to the
file server:
| Backstage URL | File Server URL |
| --------------------------------------------------------- | ------------------------------------------------------- |
| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html |
| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html |
Then deploying new sites is easy: simply copy over the `site/` folder produced
in the [Create documentation](#build-production-ready-documentation) step above
to the file server/storage solution under the ID of the documentation site. It
will then become immediately available in Backstage under the same ID as you can
see in the table above.
So, if the URL to your file server is `http://example.com/`, your
`~/hello-docs/site` folder containing the documentation should be accessible at
`http://example.com/hello-docs/`.
## Configure TechDocs to read from file server
In order for Backstage to show your documentation, it needs to know where you
uploaded it.
Make sure you have Backstage set up using
[TechDocs getting started](getting-started.md).
To point Backstage to your docs storage, add or change the following lines in
your Backstage `app-config.yaml`:
```yaml
techdocs:
storageUrl: http://example.com
```
You can now start Backstage using `yarn start` and open up your browser at
`http://localhost:3000/docs/hello-docs` to view your docs.
+17 -17
View File
@@ -3,16 +3,6 @@ id: getting-started
title: Getting Started
---
> TechDocs is not yet feature complete - currently you can't set up a complete
> end-to-end working TechDocs plugin without customizing the plugin itself.
> What you can expect from TechDocs V.0 is a demonstration of how to integrate
> docs into Backstage. TechDocs can create docs using
> [mkdocs](https://www.mkdocs.org/), as well as read published docs. If you
> publish generated docs and pass in a `storageUrl` in your `app-config.yaml`,
> you can view them in Backstage by going to
> `http://localhost:3000/docs/<remote-folder>`.
TechDocs functions as a plugin to Backstage, so you will need to use Backstage
to use TechDocs.
@@ -48,8 +38,8 @@ containing your new Backstage application.
## Installing TechDocs
TechDocs is not provided with the Backstage application by default, so you will
now need to set up TechDocs manually. It should take less than a minute.
TechDocs is provided with the Backstage application by default. If you want to
set up TechDocs manually, keep follow the instructions below.
### Adding the package
@@ -84,19 +74,29 @@ export { plugin as TechDocs } from '@backstage/plugin-techdocs';
### Setting the configuration
TechDocs allows for configuration of the docs storage URL through your
`app-config` file. The URL provided here is for demo docs to use for testing
purposes.
`app-config` file.
To use the demo docs, add the following lines to `app-config.yaml`:
The default storage URL:
```yaml
techdocs:
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
storageUrl: http://localhost:7000/techdocs/static/docs
```
If you want to configure this to point to another storage URL, change the value
of `storageUrl`.
## Run Backstage locally
Change folder to your Backstage application root and run the following command:
Change folder to `<backstage-project-root>/packages/backend` and run the
following command:
```bash
yarn start
```
Open a new command line window. Change directory to your Backstage application
root and run the following command:
```bash
yarn start
+144
View File
@@ -0,0 +1,144 @@
---
id: adopting
title: Strategies for adopting
---
This document outlines some general best practices that have been key to
Backstage's success inside Spotify. Every organization is different and some of
these learnings will therefore not be applicable for your company. We are hoping
that this can become a living document, and strongly encourage you to contribute
back whatever learnings you gather while adopting Backstage inside your company.
## Organizational setup
The true value of Backstage is unlocked when it becomes _THE_ developer portal
at your company. As such it is important to recognize that you will need a
central team that owns your Backstage deployment and treats it like a product.
This team will have **four** primary objectives:
1. Maintain and operate your deployment of Backstage. This includes customer
support, infrastructure, CI/CD and, as your Backstage product grows, on-call
support.
2. Drive adoption of customers (developers at your company).
3. Work with senior tech leadership and architects to ensure your organizations
best practices for software development are encoded into a set of
[Software Templates](../features/software-templates/index.md).
4. Evangelize Backstage as a central platform towards other
infrastructure/platform teams.
## Internal evangelization
The last objective deserves more attention, since it is the least obvious, but
also the most critical to successfully creating a consolidated platform. When
done right, Backstage acts as a "platform of platforms" or marketplace between
infra/platform teams and end-users:
![pop](../assets/pop.png)
While anyone at your company can contribute to the platform, the vast majority
of work will be done by teams that also has internal engineers as their
customers. The central team should treat these _contributing teams_ as customers
of the platform as well.
These teams should be able to autonomously deliver value directly to their
customers. This is done primarily by building [plugins](../plugins/index.md).
Contributing teams should themselves treat their plugins as, or part of, the
products they maintain.
> Case study: Inside Spotify we have a team that owns our CI platform. They
> don't only maintain the pipelines and build servers, but also expose their
> product in Backstage through a plugin. Since they also
> [maintain their own API](../plugins/call-existing-api.md), they can improve
> their product by iterating on API and UI in lockstep. Because the plugin
> follows our [platform design guidelines](../dls/design.md) their customers get
> a CI experience that is consistent with other tools on the platform (and users
> don't have to become experts in Jenkins).
### Tactics
Example of tactics we have used to evangelize Backstage internally:
- Arrange "Lunch & Learns" and seminars. Frequently offer teams interested in
Backstage development to come to a seminar where you show, for example, how to
build a plugin from scratch.
- Embedding. As contributing teams start development of their first plugin it is
often very appreciated to have one person from the central team come over and
"embed" for a Sprint or two.
- Hack days. Backstage-focused Hackathons or hack days is a fun way to get
people into plugin development.
- Show & tell meetings. In order to build an internal community around Backstage
we have quarterly meetings where anyone working on Backstage is invited to
present their work. This is a not only a great way to get early feedback, but
also helps coordination between teams that are building overlapping
experiences.
- Provide metrics. Add instrumentation to your Backstage deployment and make
metrics available to contributing teams. At Spotify we have even gone so far
as sending out weekly digest email showing how usage metrics have changed for
individual plugins.
- Pro-actively identify new plugins. Reach out to teams that own internal UIs or
platforms that you think would make sense to consolidate into Backstage.
## Metrics
These are some of the metrics that you can use to verify if Backstage has a
successful impact on your software development process:
- **Onboarding time** Time until new engineers are productive. At Spotify we
measure this as the time until the employee has merged their 10th PR (this
metric was down 55% two years after deploying Backstage).
- **Number of merges per developer/day** Less time spent jumping between
different tools and looking for information means more time to focus on
shipping code. A second level of bottlenecks can be identified if you
categorize contributions by domain (services, web, data, etc).
- **Deploys to production** Cousin to the metric above: How many times does an
engineer push changes into production.
- **MTTR** With clear ownership of all the pieces in your micro services
ecosystem and all tools integrated into one place, Backstage makes it quicker
for teams to find the root cause of failures, and fix them.
- **Context switching** Reducing context switching can help engineers stay in
the "zone". We measure the number of different tools an engineer have to
interact with in order to get a certain job done (e.g. push a change, follow
it into production and validate it did not break anything).
- **T-shapedness** A
[T-shaped](https://medium.com/@jchyip/why-t-shaped-people-e8706198e437)
engineer is someone that is able to contribute to different domains of
engineering. Teams with T-shaped people have fewer bottlenecks and can
therefore deliver more consistently. Backstage makes it easier to be T-shaped
since tools and infrastructure is consistent between domains, and information
is available centrally.
- **eNPS** Surveys asking about how productive people feel, how easy it is to
find information and overall satisfaction with internal tools.
- **Fragmentation** _(Experimental)_ Backstage
[Software Templates](../features/software-templates/index.md) helps drive
standardization in your software ecosystem. By measuring the variance in
technology between different software components it is possible to get a sense
of the overall fragmentation in your ecosystem. Examples could include:
framework versions, languages, deployment methods and various code quality
measurements.
Additionally, these proxy metrics can be used to validate the success of
Backstage as _the_ platform:
- Nr of teams that have contributed at least one plugin (currently 63 inside
Spotify)
- Nr of total plugins (currently 135 inside Spotify)
- % of contributions coming from outside the central Backstage team (currently
85% inside Spotify)
+173 -2
View File
@@ -1,6 +1,177 @@
---
id: call-existing-api
title: Call existing API
title: Call Existing API
---
## TODO
This article describes the various options that Backstage frontend plugins have,
in communicating with service APIs that already exist. Each section below
describes a possible choice, and the circumstances under which it fits.
In these examples, we will be ultimately requesting data from the fictional
FrobsCo API.
## Issuing Requests Directly
The most basic choice available is to issue requests directly from the plugin
frontend code to the FrobsCo API, using for example `fetch` or a support library
such as `axios`.
Example:
```ts
// Inside your component
fetch('https://api.frobsco.com/v1/list')
.then(response => response.json())
.then(payload => setFrobs(payload as Frob[]));
```
Internally at Spotify, this has not been a very common choice. Third party APIs
are sometimes accessed like this. Just a handful of internal APIs also went
through the trouble of exposing themselves in a way that is useful directly from
a browser, but even then, often not from the public internet but only supporting
users that are already on the company VPN.
This can be used when:
- The API already does/exposes exactly what you need.
- The request/response patterns of the API match real world usage needs in
Backstage frontend plugins. For example, if the end use case is to show a
small summary in Backstage, but the only available API endpoint gives a 30
megabyte blob with large amounts of redundant information, it would hurt the
end user experience. Particularly on mobile. The same goes for cases where you
want to show many individual pieces of information: if a common use case is to
show large tables where one API request per cell is necessary, the browser
will quickly become swamped and you may want to consider performing
aggregation elsewhere instead.
- The API can maintain interactive request/response times at your required peak
request rates. The end user experience will be degraded if they spend a lot of
time waiting for the data to arrive.
- The API endpoint is highly available. The browser does not have builtin
facilities for load balancing, service discovery, retries, health checks,
circuit breaking and similar. If the endpoint is occasionally down even for
short periods of time (e.g. during deploys), end users will quickly notice.
- The API is exposed over HTTPS (not just HTTP), and properly handles
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). These are
requirements that the user's browser will impose for security reasons, and the
requests will be rejected otherwise.
- The API endpoint is easily reachable, in terms of network conditions, by end
users. This may be particularly relevant if your end users are outside of your
perimeter.
- The requests do not require secrets to be passed. This limitation does not
apply to OAuth tokens, which the frontend can negotiate and make proper use
of.
## Using The Backstage Proxy
Backstage has an optional proxy plugin for the backend, that can be used to
easily add proxy routes to downstream APIs.
Example:
```yaml
# In app-config.yaml
proxy:
'/frobs':
target: 'http://api.frobsco.com/v1'
changeOrigin: true
pathRewrite:
'^/proxy/frobs/': '/'
```
```ts
// Inside your component
const backendUrl = config.getString('backend.baseUrl');
fetch(`${backendUrl}/proxy/frobs/list`)
.then(response => response.json())
.then(payload => setFrobs(payload as Frob[]));
```
The proxy is powered by the `http-proxy-middleware` package, and supports all of
its
[configuration options](https://github.com/chimurai/http-proxy-middleware#options).
Internally at Spotify, the proxy option has been the overwhelmingly most popular
choice for plugin makers. Since we have DNS based service discovery in place and
a microservices framework that made it trivial to expose plain HTTP, it has been
a matter of just adding a few lines of Backstage config to get the benefit of
being easily and robustly reachable from users' web browsers as well.
This may be used instead of direct requests, when:
- You need to perform HTTPS termination and/or CORS handling, because the API
itself is not supplying those.
- You need to inject a simple static secret into the requests, e.g. an
Authorization header that gets added to the request headers.
- You want to make use of other proxy facilities, such as retries, failover,
health checks, routing, request logging, rewrites, etc.
- You already have the Backstage backend itself exposed through your perimeter
and find it practical to have only one entry point to deal with, governing
ingress with just the Backstage config.
## Creating a Backstage Backend Plugin
Much like the Backstage frontend, the Backstage backend also has a plugin
system. The above mentioned proxy is actually one such plugin. If you were in
need of a more involved integration than just direct access to the FrobsCo API,
or if you needed to hold state, you may want to make such a plugin.
Example:
```ts
// Inside your component
const backendUrl = config.getString('backend.baseUrl');
fetch(`${backendUrl}/frobs-aggregator/summary`)
.then(response => response.json())
.then(payload => setSummary(payload as FrobSummary));
```
```ts
// Inside a new frobs-aggregator backend plugin
router.use('/summary', async (req, res) => {
const agg = await Promise.all([
fetch('https://api.frobsco.com/v1/list'),
fetch('http://flerps.partnercompany.com:8080/flerp-batch'),
database.currentThunk(),
]).then(async ([frobs, flerps, thunk]) => {
return computeAggregate(await frobs.json(), await flerps.json(), thunk);
});
res.status(200).send(agg);
});
```
For a more detailed example, see
[the lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse)
that stores some state in a database and adds new capabilities to the underlying
API.
Internally at Spotify, this has been a fairly popular choice for different
reasons. Commonly, the backend has been used as a caching and data massaging
layer for slow APIs or APIs whose request/response shapes or speeds were not
acceptable for direct use by frontends. For example, this has made it possible
to issue efficient batch queries from the frontend, e.g. in big lists or tables
that want to resolve a lot of sparse data from the larger list that an
underlying service supplies.
This may be used instead of the above, when:
- You need to perform complex model conversion, or protocol translation beyond
what the proxy handles.
- You want to perform aggregations or summaries on the backend instead of on the
frontend.
- You want to enable batching or caching of slower or more unreliable APIs.
- You need to maintain state for your plugin, perhaps using the builtin database
support in the backend.
- You need to inject secrets or in other ways negotiate with other parts of the
API or other services in order to perform your work.
- You want to enforce end user authentication / authorization for operations on
behalf of the API, have session handling, or similar.
There is a balance to strike regarding when to make an entirely separate backend
for a purpose, and when to make a Backstage backend plugin that adapts something
that already exists. General advice is not easy to give, but contact us on
Discord if you have any questions, and we may be able to offer guidance.
## Extending the GraphQL Model
The extensible GraphQL backend layer is not built yet. This section will be
expanded when that happens. Stay tuned!
+1 -1
View File
@@ -1,6 +1,6 @@
---
id: index
title: Intro
title: Intro to plugins
---
Backstage is a single-page application composed of a set of plugins.
+2 -2
View File
@@ -55,7 +55,7 @@ ApiRef:
## githubAuth
Provides authentication towards Github APIs
Provides authentication towards GitHub APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
@@ -67,7 +67,7 @@ ApiRef:
## gitlabAuth
Provides authentication towards Gitlab APIs
Provides authentication towards GitLab APIs
Implemented types: [OAuthApi](./OAuthApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
@@ -13,7 +13,7 @@ Two days ago, we released the open source version of [Backstage](https://backsta
## Whats the big infrastructure problem?
As companies grow, their infrastructure systems get messier. Consider a team that wants to deploy something to the cloud. While Spotify has many awesome engineers, not every engineer is well-versed in our chosen cloud-provider tooling. Yet everyone is required to know and understand Terraform, GCP/AWS/Azure CLIs, Gitlab CI, Prometheus, Kubernetes, Docker, various monitoring and alerting tools, and much, much more. Once other resources come into play (databases, queueing, etc.), each engineer requires even more tools and domain-specific knowledge (or “disciplines”), from backend to machine learning, to mobile and data.
As companies grow, their infrastructure systems get messier. Consider a team that wants to deploy something to the cloud. While Spotify has many awesome engineers, not every engineer is well-versed in our chosen cloud-provider tooling. Yet everyone is required to know and understand Terraform, GCP/AWS/Azure CLIs, GitLab CI, Prometheus, Kubernetes, Docker, various monitoring and alerting tools, and much, much more. Once other resources come into play (databases, queueing, etc.), each engineer requires even more tools and domain-specific knowledge (or “disciplines”), from backend to machine learning, to mobile and data.
## Whats the fix?
+5 -2
View File
@@ -177,6 +177,9 @@
"journey": {
"title": "journey"
},
"overview/adopting": {
"title": "Strategies for adopting"
},
"overview/architecture-overview": {
"title": "Architecture overview"
},
@@ -199,7 +202,7 @@
"title": "Backend plugin"
},
"plugins/call-existing-api": {
"title": "Call existing API"
"title": "Call Existing API"
},
"plugins/create-a-plugin": {
"title": "Create a Backstage Plugin"
@@ -208,7 +211,7 @@
"title": "Existing plugins"
},
"plugins/index": {
"title": "Intro"
"title": "Intro to plugins"
},
"plugins/plugin-development": {
"title": "Plugin Development in Backstage"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.0",
"version": "0.1.1-alpha.19",
"name": "backstage-microsite",
"license": "Apache-2.0",
"private": true,
+1 -1
View File
@@ -112,7 +112,7 @@ const Background = props => {
left: 0,
right: 0,
bottom: 0,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg);`,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg)`,
}}
/>
</div>
+12
View File
@@ -0,0 +1,12 @@
const React = require('react');
const Redirect = require('../../core/Redirect.js');
const siteConfig = require(process.cwd() + '/siteConfig.js');
function Docs() {
return (
<Redirect redirect="/docs/overview/what-is-backstage" config={siteConfig} />
);
}
module.exports = Docs;
+14 -4
View File
@@ -353,10 +353,20 @@ class Index extends React.Component {
</Block.TextBox>
<Breakpoint
wide={
<img
style={{ margin: 'auto' }}
src={`${baseUrl}img/techdocs-web.png`}
/>
<Block.Graphics padding={0}>
<Block.Graphic
x={-55}
y={-5}
width={210}
src={`${baseUrl}img/techdocs-web.png`}
/>
<Block.Graphic
x={-55}
y={-5}
width={210}
src={`${baseUrl}img/techdocs2.gif`}
/>
</Block.Graphics>
}
/>
</Block.Container>
+2 -1
View File
@@ -5,7 +5,8 @@
"overview/architecture-overview",
"overview/architecture-terminology",
"overview/roadmap",
"overview/vision"
"overview/vision",
"overview/adopting"
],
"Getting Started": [
"getting-started/index",
+2 -1
View File
@@ -35,11 +35,12 @@ const siteConfig = {
// For no header links in the top nav bar -> headerLinks: [],
headerLinks: [
{
href: 'https://github.com/spotify/backstage#backstage',
href: 'https://github.com/spotify/backstage',
label: 'GitHub',
},
{
doc: 'overview/what-is-backstage',
href: '/docs',
label: 'Docs',
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+2 -5
View File
@@ -8,6 +8,7 @@ nav:
- Architecture and terminology: 'overview/architecture-terminology.md'
- Roadmap: 'overview/roadmap.md'
- Vision: 'overview/vision.md'
- Strategies for adopting: 'overview/adopting.md'
- Getting started:
- Running Backstage locally: 'getting-started/index.md'
- Installation: 'getting-started/installation.md'
@@ -41,11 +42,7 @@ nav:
- Overview: 'features/techdocs/README.md'
- Getting Started: 'features/techdocs/getting-started.md'
- Concepts: 'features/techdocs/concepts.md'
- Reading Documentation: 'features/techdocs/reading-documentation.md'
- Writing Documentation: 'features/techdocs/writing-documentation.md'
- Publishing Documentation: 'features/techdocs/publishing-documentation.md'
- Contributing: 'features/techdocs/contributing.md'
- Debugging: 'features/techdocs/debugging.md'
- Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md'
- FAQ: 'features/techdocs/FAQ.md'
- Plugins:
- Overview: 'plugins/index.md'
+23 -23
View File
@@ -1,34 +1,34 @@
{
"name": "example-app",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/plugin-api-docs": "^0.1.1-alpha.18",
"@backstage/plugin-catalog": "^0.1.1-alpha.18",
"@backstage/plugin-circleci": "^0.1.1-alpha.18",
"@backstage/plugin-explore": "^0.1.1-alpha.18",
"@backstage/plugin-github-actions": "^0.1.1-alpha.18",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.18",
"@backstage/plugin-graphiql": "^0.1.1-alpha.18",
"@backstage/plugin-jenkins": "^0.1.1-alpha.18",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.18",
"@backstage/plugin-newrelic": "^0.1.1-alpha.18",
"@backstage/plugin-register-component": "^0.1.1-alpha.18",
"@backstage/plugin-rollbar": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.18",
"@backstage/plugin-sentry": "^0.1.1-alpha.18",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.18",
"@backstage/plugin-techdocs": "^0.1.1-alpha.18",
"@backstage/plugin-welcome": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/plugin-api-docs": "^0.1.1-alpha.19",
"@backstage/plugin-catalog": "^0.1.1-alpha.19",
"@backstage/plugin-circleci": "^0.1.1-alpha.19",
"@backstage/plugin-explore": "^0.1.1-alpha.19",
"@backstage/plugin-github-actions": "^0.1.1-alpha.19",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.19",
"@backstage/plugin-graphiql": "^0.1.1-alpha.19",
"@backstage/plugin-jenkins": "^0.1.1-alpha.19",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.19",
"@backstage/plugin-newrelic": "^0.1.1-alpha.19",
"@backstage/plugin-register-component": "^0.1.1-alpha.19",
"@backstage/plugin-rollbar": "^0.1.1-alpha.19",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.19",
"@backstage/plugin-sentry": "^0.1.1-alpha.19",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.19",
"@backstage/plugin-techdocs": "^0.1.1-alpha.19",
"@backstage/plugin-welcome": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-pull-requests": "0.3.0",
"@roadiehq/backstage-plugin-travis-ci": "^0.1.3",
"@roadiehq/backstage-plugin-travis-ci": "^0.1.4",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
+4 -4
View File
@@ -37,14 +37,14 @@ export const providers = [
},
{
id: 'gitlab-auth-provider',
title: 'Gitlab',
message: 'Sign In using Gitlab',
title: 'GitLab',
message: 'Sign In using GitLab',
apiRef: gitlabAuthApiRef,
},
{
id: 'github-auth-provider',
title: 'Github',
message: 'Sign In using Github',
title: 'GitHub',
message: 'Sign In using GitHub',
apiRef: githubAuthApiRef,
},
{
+7 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config-loader": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/config-loader": "^0.1.1-alpha.19",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -43,7 +43,8 @@
"lodash": "^4.17.15",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"selfsigned": "^1.10.7"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -54,7 +55,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
@@ -19,7 +19,7 @@ import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { useHotCleanup } from '../../hot';
@@ -30,7 +30,13 @@ import {
requestLoggingHandler,
} from '../../middleware';
import { ServiceBuilder } from '../types';
import { readBaseOptions, readCorsOptions } from './config';
import {
readBaseOptions,
readCorsOptions,
readHttpsSettings,
HttpsSettings,
} from './config';
import { createHttpServer, createHttpsServer } from './hostFactory';
const DEFAULT_PORT = 7000;
// '' is express default, which listens to all interfaces
@@ -41,6 +47,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private httpsSettings: HttpsSettings | undefined;
private routers: [string, Router][];
// Reference to the module where builder is created - needed for hot module
// reloading
@@ -70,6 +77,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
this.corsOptions = corsOptions;
}
const httpsSettings = readHttpsSettings(backendConfig);
if (httpsSettings) {
this.httpsSettings = httpsSettings;
}
return this;
}
@@ -88,6 +100,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
setHttpsSettings(settings: HttpsSettings): ServiceBuilder {
this.httpsSettings = settings;
return this;
}
enableCors(options: cors.CorsOptions): ServiceBuilder {
this.corsOptions = options;
return this;
@@ -98,9 +115,15 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
start(): Promise<Server> {
start(): Promise<http.Server> {
const app = express();
const { port, host, logger, corsOptions } = this.getOptions();
const {
port,
host,
logger,
corsOptions,
httpsSettings,
} = this.getOptions();
app.use(helmet());
if (corsOptions) {
@@ -121,20 +144,24 @@ export class ServiceBuilderImpl implements ServiceBuilder {
reject(e);
});
const server = stoppable(
app.listen(port, host, () => {
const server: http.Server = httpsSettings
? createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
server.stop((e: any) => {
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(server);
resolve(stoppableServer);
});
}
@@ -143,12 +170,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
httpsSettings?: HttpsSettings;
} {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
httpsSettings: this.httpsSettings,
};
}
}
@@ -22,6 +22,41 @@ export type BaseOptions = {
listenHost?: string;
};
export type CertificateOptions = {
key?: CertificateKeyOptions;
attributes?: CertificateAttributeOptions;
};
export type CertificateKeyOptions = {
size?: number;
algorithm?: string;
days?: number;
};
export type CertificateAttributeOptions = {
commonName?: string;
};
export type HttpsSettings = {
certificate: CertificateSigningOptions | CertificateReferenceOptions;
};
export type CertificateReferenceOptions = {
key: string;
cert: string;
};
export type CertificateSigningOptions = {
algorithm: string;
size?: number;
days?: number;
attributes?: CertificateAttributes;
};
export type CertificateAttributes = {
commonName?: string;
};
/**
* Reads some base options out of a config object.
*
@@ -40,6 +75,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
if (typeof config.get('listen') === 'string') {
// TODO(freben): Expand this to support more addresses and perhaps optional
const { host, port } = parseListenAddress(config.getString('listen'));
return removeUnknown({
listenPort: port,
listenHost: host,
@@ -49,6 +85,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
return removeUnknown({
listenPort: config.getOptionalNumber('listen.port'),
listenHost: config.getOptionalString('listen.host'),
baseUrl: config.getOptionalString('baseUrl'),
});
}
@@ -86,6 +123,39 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
});
}
/**
* Attempts to read a https settings object from the root of a config object.
*
* @param config The root of a backend config object
* @returns A https settings object, or undefined if not specified
*
* @example
* ```json
* {
* https: {
* certificate: ...
* }
* }
* ```
*/
export function readHttpsSettings(
config: ConfigReader,
): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
if (!cc) {
return undefined;
}
const certificateConfig = cc.get('certificate');
const cfg = {
certificate: certificateConfig,
};
return removeUnknown(cfg as HttpsSettings);
}
function getOptionalStringOrStrings(
config: ConfigReader,
key: string,
@@ -0,0 +1,92 @@
/*
* 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 express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Logger } from 'winston';
import { HttpsSettings } from './config';
/**
* Creates a Http server instance based on an Express application.
*
* @param app The Express application object
* @param logger Optional Winston logger object
* @returns A Http server instance
*
*/
export function createHttpServer(
app: express.Express,
logger?: Logger,
): http.Server {
logger?.info('Initializing http server');
return http.createServer(app);
}
/**
* Creates a Https server instance based on an Express application.
*
* @param app The Express application object
* @param httpsSettings HttpsSettings for self-signed certificate generation
* @param logger Optional Winston logger object
* @returns A Https server instance
*
*/
export function createHttpsServer(
app: express.Express,
httpsSettings: HttpsSettings,
logger?: Logger,
): http.Server {
logger?.info('Initializing https server');
const credentials: { key: string; cert: string } = {
key: '',
cert: '',
};
const signingOptions: any = httpsSettings?.certificate;
if (signingOptions?.algorithm !== undefined) {
logger?.info('Generating self-signed certificate with attributes');
const certificateAttributes: Array<any> = Object.entries(
signingOptions.attributes,
).map(([name, value]) => ({ name, value }));
// TODO: Create a type def for selfsigned.
const signatures = require('selfsigned').generate(certificateAttributes, {
algorithm: signingOptions?.algorithm,
keySize: signingOptions?.size || 2048,
days: signingOptions?.days || 30,
});
logger?.info('Bootstrapping self-signed certificate');
credentials.key = signatures.private;
credentials.cert = signatures.cert;
} else {
logger?.info('Bootstrapping cert from config');
credentials.key = signingOptions?.key;
credentials.cert = signingOptions?.cert;
}
if (credentials.key === '' || credentials.cert === '') {
throw new Error('Invalid credentials');
}
return https.createServer(credentials, app) as http.Server;
}
@@ -19,6 +19,7 @@ import cors from 'cors';
import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
import { HttpsSettings } from './lib/config';
export type ServiceBuilder = {
/**
@@ -39,6 +40,15 @@ export type ServiceBuilder = {
*/
setPort(port: number): ServiceBuilder;
/**
* Sets the host to listen on.
*
* '' is express default, which listens to all interfaces.
*
* @param host The host to listen on
*/
setHost(host: string): ServiceBuilder;
/**
* Sets the logger to use for service-specific logging.
*
@@ -58,6 +68,15 @@ export type ServiceBuilder = {
*/
enableCors(options: cors.CorsOptions): ServiceBuilder;
/**
* Configure self-signed certificate generation options.
*
* If this method is not called, the resulting service will use sensible defaults
*
* @param options Standard certificate options
*/
setHttpsSettings(settings: HttpsSettings): ServiceBuilder;
/**
* Adds a router (similar to the express .use call) to the service.
*
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,18 +18,18 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.18",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.18",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.18",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.18",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.18",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.18",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.18",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.18",
"@backstage/backend-common": "^0.1.1-alpha.19",
"@backstage/catalog-model": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.19",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.19",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.19",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.19",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.19",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.19",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.19",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.19",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.19",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
@@ -40,7 +40,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+11 -6
View File
@@ -20,22 +20,27 @@ import {
Preparers,
Generators,
LocalPublish,
TechdocsGenerator
TechdocsGenerator,
GithubPreparer,
} from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
import Docker from 'dockerode';
export default async function createPlugin({ logger, config }: PluginEnvironment) {
export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator();
const techdocsGenerator = new TechdocsGenerator(logger);
generators.register('techdocs', techdocsGenerator);
const directoryPreparer = new DirectoryPreparer();
const preparers = new Preparers();
const githubPreparer = new GithubPreparer(logger);
const directoryPreparer = new DirectoryPreparer(logger);
preparers.register('dir', directoryPreparer);
preparers.register('github', githubPreparer);
const publisher = new LocalPublish();
const publisher = new LocalPublish(logger);
const dockerClient = new Docker();
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
@@ -29,7 +29,6 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0"
},
+5 -2
View File
@@ -37,8 +37,11 @@ async function getConfig() {
// TODO: jest is working on module support, it's possible that we can remove this in the future
transform: {
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
'\\.(js|jsx|ts|tsx)': require.resolve('ts-jest'),
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)': require.resolve(
'\\.(js|jsx|ts|tsx)$': [
require.resolve('ts-jest'),
{ isolatedModules: true },
],
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve(
'./jestFileTransform.js',
),
},
+6 -11
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public"
@@ -21,7 +21,6 @@
"build": "backstage-cli build --outputs cjs",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"test:e2e": "node e2e-test/cli-e2e-test.js",
"clean": "backstage-cli clean",
"start": "nodemon --"
},
@@ -29,9 +28,9 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config-loader": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/config-loader": "^0.1.1-alpha.19",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
@@ -69,9 +68,7 @@
"jest-css-modules": "^2.1.0",
"jest-esm-transformer": "^1.0.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.0",
"ora": "^4.0.3",
"pgtools": "^0.3.0",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^10.2.1",
@@ -79,7 +76,7 @@
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
"rollup": "2.23.x",
"rollup-plugin-dts": "^1.4.6",
"rollup-plugin-dts": "1.4.11",
"rollup-plugin-esbuild": "^2.0.0",
"rollup-plugin-image-files": "^1.4.2",
"rollup-plugin-peer-deps-external": "^2.2.2",
@@ -118,9 +115,7 @@
"@types/webpack-dev-server": "^3.10.0",
"del": "^5.1.0",
"nodemon": "^2.0.2",
"tree-kill": "^1.2.2",
"ts-node": "^8.6.2",
"zombie": "^6.1.4"
"ts-node": "^8.6.2"
},
"files": [
"asset-types",
+2
View File
@@ -25,9 +25,11 @@ export default async (cmd: Command) => {
env: 'development',
rootPaths: [paths.targetRoot, paths.targetDir],
});
const waitForExit = await serveBackend({
entry: 'src/index',
checksEnabled: cmd.check,
inspectEnabled: cmd.inspect,
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
+1 -1
View File
@@ -22,7 +22,7 @@ export default async (cmd: Command, cmdArgs: string[]) => {
const args = [
'--ext=js,jsx,ts,tsx',
'--max-warnings=0',
'--format=eslint-formatter-friendly',
`--format=${cmd.format}`,
...(cmdArgs ?? [paths.targetDir]),
];
if (cmd.fix) {
+6
View File
@@ -52,6 +52,7 @@ const main = (argv: string[]) => {
.command('backend:dev')
.description('Start local development server with HMR for the backend')
.option('--check', 'Enable type checking and linting')
.option('--inspect', 'Enable debugger')
.action(lazyAction(() => import('./commands/backend/dev'), 'default'));
program
@@ -113,6 +114,11 @@ const main = (argv: string[]) => {
program
.command('lint')
.option(
'--format <format>',
'Lint report output format',
'eslint-formatter-friendly',
)
.option('--fix', 'Attempt to automatically fix violations')
.description('Lint a package')
.action(lazyAction(() => import('./commands/lint'), 'default'));
+5 -1
View File
@@ -19,7 +19,11 @@ import { createBackendConfig } from './config';
import { resolveBundlingPaths } from './paths';
import { ServeOptions } from './types';
export async function serveBackend(options: ServeOptions) {
export async function serveBackend(
options: ServeOptions & {
inspectEnabled: boolean;
},
) {
const paths = resolveBundlingPaths(options);
const config = createBackendConfig(paths, {
...options,
+9 -1
View File
@@ -198,9 +198,17 @@ export function createBackendConfig(
chunkFilename: isDev
? '[name].chunk.js'
: '[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]',
}
: {}),
},
plugins: [
new StartServerPlugin('main.js'),
new StartServerPlugin({
name: 'main.js',
nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined,
}),
new webpack.HotModuleReplacementPlugin(),
...(checksEnabled
? [
+3 -1
View File
@@ -25,7 +25,9 @@ export type BundlingOptions = {
baseUrl: URL;
};
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'>;
export type BackendBundlingOptions = Omit<BundlingOptions, 'baseUrl'> & {
inspectEnabled: boolean;
};
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
+4 -2
View File
@@ -30,10 +30,12 @@ export type AppConfig = {
};
export type Config = {
has(key: string): boolean;
keys(): string[];
get(key: string): JsonValue;
getOptional(key: string): JsonValue | undefined;
get(key?: string): JsonValue;
getOptional(key?: string): JsonValue | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): Config | undefined;
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,8 +29,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -41,8 +41,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/test-utils-core": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/test-utils-core": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -222,7 +222,7 @@ export const googleAuthApiRef = createApiRef<
});
/**
* Provides authentication towards Github APIs.
* Provides authentication towards GitHub APIs.
*
* See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
* for a full list of supported scopes.
@@ -231,7 +231,7 @@ export const githubAuthApiRef = createApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.github',
description: 'Provides authentication towards Github APIs',
description: 'Provides authentication towards GitHub APIs',
});
/**
@@ -252,7 +252,7 @@ export const oktaAuthApiRef = createApiRef<
});
/**
* Provides authentication towards Gitlab APIs.
* Provides authentication towards GitLab APIs.
*
* See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token
* for a full list of supported scopes.
@@ -261,7 +261,7 @@ export const gitlabAuthApiRef = createApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi
>({
id: 'core.auth.gitlab',
description: 'Provides authentication towards Gitlab APIs',
description: 'Provides authentication towards GitLab APIs',
});
/**
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/core-api": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.19",
"@backstage/core-api": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -50,12 +50,12 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^13.2.1",
"react-syntax-highlighter": "^13.5.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+4 -4
View File
@@ -48,7 +48,7 @@ export interface TabsProps {
tabs: TabProps[];
}
const useStyles = makeStyles<BackstageTheme>((theme: BackstageTheme) => ({
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
flexGrow: 1,
width: '100%',
@@ -66,7 +66,7 @@ const useStyles = makeStyles<BackstageTheme>((theme: BackstageTheme) => ({
export const Tabs: FC<TabsProps> = ({ tabs }) => {
const classes = useStyles();
const [value, setValue] = useState([0, 0]); // [selectedChunckedNavIndex, selectedIndex]
const [value, setValue] = useState([0, 0]); // [selectedChunkedNavIndex, selectedIndex]
const [navIndex, setNavIndex] = useState(0);
const [numberOfChunkedElement, setNumberOfChunkedElement] = useState(0);
const [chunkedTabs, setChunkedTabs] = useState<TabProps[][]>([[]]);
@@ -89,7 +89,7 @@ export const Tabs: FC<TabsProps> = ({ tabs }) => {
const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length;
useEffect(() => {
// Each time the window is resized we calculate how many tabs wwe can render given the window width
// Each time the window is resized we calculate how many tabs we can render given the window width
const padding = 20; // The AppBar padding
const numberOfTabIcons = navIndex === 0 ? 1 : 2;
@@ -99,7 +99,7 @@ export const Tabs: FC<TabsProps> = ({ tabs }) => {
const newChunkedElementSize = Math.floor(wrapperWidth / 170);
setNumberOfChunkedElement(newChunkedElementSize);
setChunkedTabs(chunkArray([...tabs], newChunkedElementSize));
setChunkedTabs(chunkArray(tabs, newChunkedElementSize));
setValue([
Math.floor(flattenIndex / newChunkedElementSize),
flattenIndex % newChunkedElementSize,
+11 -10
View File
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TabProps } from './Tabs';
export const chunkArray = (
myArray: TabProps[],
chunkSize: number,
): TabProps[][] => {
const results = [];
while (myArray.length) {
results.push(myArray.splice(0, chunkSize));
export function chunkArray<T>(array: T[], chunkSize: number): T[][] {
if (chunkSize <= 0) {
return [array];
}
return results;
};
const result: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
@@ -70,14 +70,14 @@ export function SidebarUserSettings() {
)}
{providers.includes('github') && (
<OAuthProviderSettings
title="Github"
title="GitHub"
apiRef={githubAuthApiRef}
icon={Star}
/>
)}
{providers.includes('gitlab') && (
<OAuthProviderSettings
title="Gitlab"
title="GitLab"
apiRef={gitlabAuthApiRef}
icon={Star}
/>
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.18",
"@backstage/cli-common": "^0.1.1-alpha.19",
"chalk": "^4.0.0",
"commander": "^4.1.1",
"fs-extra": "^9.0.0",
@@ -54,3 +54,5 @@ catalog:
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml
- type: github
target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml
- type: github
target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
@@ -14,15 +14,15 @@ export default async function createPlugin({
config,
}: PluginEnvironment) {
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator();
const techdocsGenerator = new TechdocsGenerator(logger);
generators.register('techdocs', techdocsGenerator);
const directoryPreparer = new DirectoryPreparer();
const directoryPreparer = new DirectoryPreparer(logger);
const preparers = new Preparers();
preparers.register('dir', directoryPreparer);
const publisher = new LocalPublish();
const publisher = new LocalPublish(logger);
const dockerClient = new Docker();
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": true,
"homepage": "https://backstage.io",
"repository": {
+14 -4
View File
@@ -15,7 +15,11 @@
*/
import ts from 'typescript';
import { relative } from 'path';
import {
relative as relativePath,
sep as pathSep,
posix as posixPath,
} from 'path';
import {
ExportedInstance,
ApiDoc,
@@ -25,6 +29,12 @@ import {
TypeLink,
} from './types';
// Always use unix path separators for the relative file
// paths, since we'll be using those for HTTP URLs.
function relativeLink(basePath: string, filePath: string) {
return relativePath(basePath, filePath).split(pathSep).join(posixPath.sep);
}
/**
* The ApiDocGenerator uses the typescript compiler API to build the data structure that
* describes a Backstage API and all of it's related types.
@@ -58,7 +68,7 @@ export default class ApiDocGenerator {
const id = this.getObjectPropertyLiteral(info, 'id');
const description = this.getObjectPropertyLiteral(info, 'description');
const file = relative(this.basePath, source.fileName);
const file = relativeLink(this.basePath, source.fileName);
const { line } = source.getLineAndCharacterOfPosition(
apiInstance.node.getStart(),
);
@@ -111,7 +121,7 @@ export default class ApiDocGenerator {
const name = (type.aliasSymbol || type.symbol).name;
const [declaration] = (type.aliasSymbol || type.symbol).declarations;
const sourceFile = declaration.getSourceFile();
const file = relative(this.basePath, sourceFile.fileName);
const file = relativeLink(this.basePath, sourceFile.fileName);
const { line } = sourceFile.getLineAndCharacterOfPosition(
declaration.getStart(),
);
@@ -234,7 +244,7 @@ export default class ApiDocGenerator {
const { line } = sourceFile.getLineAndCharacterOfPosition(
declaration.getStart(),
);
const file = relative(this.basePath, sourceFile.fileName);
const file = relativeLink(this.basePath, sourceFile.fileName);
const typeInfo = {
id: (symbol as any).id,
name: symbol.name,
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
ignorePatterns: ['templates/**'],
rules: {
'no-console': 0,
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: false,
peerDependencies: false,
bundledDependencies: false,
},
],
},
};
+24
View File
@@ -0,0 +1,24 @@
# e2e-test
End-to-end test for verifying Backstage packages.
## Usage
This package is only meant for usage within the Backstage monorepo.
All packages need to be installed and built before running the test. In a fresh clone of this repo you first need to run the following from the repo root:
```sh
yarn install
yarn tsc
yarn build
```
Once those tasks have completed, you can now run the test using `yarn start` inside this package.
If you make changes to other packages you will need to rerun `yarn tsc && yarn build`. Changes to this package do not require a rebuild.
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
+35
View File
@@ -0,0 +1,35 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.0.0",
"private": true,
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/spotify/backstage",
"directory": "packages/e2e-test"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "src/index.js",
"scripts": {
"start": "node .",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"test:e2e": "yarn start"
},
"devDependencies": {
"@backstage/cli-common": "^0.1.1-alpha.19",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
"node-fetch": "^2.6.0",
"pgtools": "^0.3.0",
"tree-kill": "^1.2.2",
"ts-node": "^8.6.2",
"zombie": "^6.1.4"
}
}
@@ -14,14 +14,14 @@
* limitations under the License.
*/
const os = require('os');
const fs = require('fs-extra');
const fetch = require('node-fetch');
const handlebars = require('handlebars');
const killTree = require('tree-kill');
const { resolve: resolvePath, join: joinPath } = require('path');
const Browser = require('zombie');
const {
import os from 'os';
import fs from 'fs-extra';
import fetch from 'node-fetch';
import handlebars from 'handlebars';
import killTree from 'tree-kill';
import { resolve as resolvePath, join as joinPath } from 'path';
import Browser from 'zombie';
import {
spawnPiped,
runPlain,
handleError,
@@ -29,8 +29,17 @@ const {
waitFor,
waitForExit,
print,
} = require('./helpers');
const pgtools = require('pgtools');
} from './helpers';
import pgtools from 'pgtools';
import { findPaths } from '@backstage/cli-common';
const paths = findPaths(__dirname);
const templatePackagePaths = [
'packages/cli/templates/default-plugin/package.json.hbs',
'packages/create-app/templates/default-app/packages/app/package.json.hbs',
'packages/create-app/templates/default-app/packages/backend/package.json.hbs',
];
async function main() {
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
@@ -59,38 +68,24 @@ async function main() {
/**
* Builds a dist workspace that contains the cli and core packages
*/
async function buildDistWorkspace(workspaceName, rootDir) {
async function buildDistWorkspace(workspaceName: string, rootDir: string) {
const workspaceDir = resolvePath(rootDir, workspaceName);
await fs.ensureDir(workspaceDir);
// We grab the needed dependencies from the template packages
const appPkgTemplate = await fs.readFile(
resolvePath(
__dirname,
'../../create-app/templates/default-app/packages/app/package.json.hbs',
),
'utf8',
);
const appPkg = JSON.parse(
handlebars.compile(appPkgTemplate)({ version: '0.0.0' }),
);
const appDeps = Object.keys(appPkg.dependencies).filter(name =>
name.startsWith('@backstage/'),
);
// We grab the needed dependencies from the create app template
const createAppDeps = new Set<string>();
for (const pkgJsonPath of templatePackagePaths) {
const path = paths.resolveOwnRoot(pkgJsonPath);
const pkgTemplate = await fs.readFile(path, 'utf8');
const { dependencies = {}, devDependencies = {} } = JSON.parse(
handlebars.compile(pkgTemplate)({ version: '0.0.0' }),
);
const backendPkgTemplate = await fs.readFile(
resolvePath(
__dirname,
'../../create-app/templates/default-app/packages/backend/package.json.hbs',
),
'utf8',
);
const backendPkg = JSON.parse(
handlebars.compile(backendPkgTemplate)({ version: '0.0.0' }),
);
const backendDeps = Object.keys(backendPkg.dependencies).filter(name =>
name.startsWith('@backstage/'),
);
Array<string>()
.concat(Object.keys(dependencies), Object.keys(devDependencies))
.filter(name => name.startsWith('@backstage/'))
.forEach(dep => createAppDeps.add(dep));
}
print(`Preparing workspace`);
await runPlain([
@@ -98,13 +93,8 @@ async function buildDistWorkspace(workspaceName, rootDir) {
'backstage-cli',
'build-workspace',
workspaceDir,
'@backstage/cli',
'@backstage/create-app',
'@backstage/core',
'@backstage/dev-utils',
'@backstage/test-utils',
...appDeps,
...backendDeps,
...createAppDeps,
]);
print('Pinning yarn version in workspace');
@@ -121,14 +111,19 @@ async function buildDistWorkspace(workspaceName, rootDir) {
/**
* Pin the yarn version in a directory to the one we're using in the Backstage repo
*/
async function pinYarnVersion(dir) {
const repoRoot = resolvePath(__dirname, '../../..');
const yarnRc = await fs.readFile(resolvePath(repoRoot, '.yarnrc'), 'utf8');
async function pinYarnVersion(dir: string) {
const yarnRc = await fs.readFile(paths.resolveOwnRoot('.yarnrc'), 'utf8');
const yarnRcLines = yarnRc.split('\n');
const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarn-path'));
const [, localYarnPath] = yarnPathLine.match(/"(.*)"/);
const yarnPath = resolvePath(repoRoot, localYarnPath);
if (!yarnPathLine) {
throw new Error(`Unable to find 'yarn-path' in ${yarnRc}`);
}
const match = yarnPathLine.match(/"(.*)"/);
if (!match) {
throw new Error(`Invalid 'yarn-path' in ${yarnRc}`);
}
const [, localYarnPath] = match;
const yarnPath = paths.resolveOwnRoot(localYarnPath);
await fs.writeFile(resolvePath(dir, '.yarnrc'), `yarn-path "${yarnPath}"\n`);
}
@@ -136,7 +131,12 @@ async function pinYarnVersion(dir) {
/**
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
*/
async function createApp(appName, isPostgres, workspaceDir, rootDir) {
async function createApp(
appName: string,
isPostgres: boolean,
workspaceDir: string,
rootDir: string,
) {
const child = spawnPiped(
[
'node',
@@ -150,20 +150,20 @@ async function createApp(appName, isPostgres, workspaceDir, rootDir) {
try {
let stdout = '';
child.stdout.on('data', data => {
child.stdout?.on('data', data => {
stdout = stdout + data.toString('utf8');
});
await waitFor(() => stdout.includes('Enter a name for the app'));
child.stdin.write(`${appName}\n`);
child.stdin?.write(`${appName}\n`);
await waitFor(() => stdout.includes('Select database for the backend'));
if (!isPostgres) {
// Simulate down arrow press
child.stdin.write(`\u001B\u005B\u0042`);
child.stdin?.write(`\u001B\u005B\u0042`);
}
child.stdin.write(`\n`);
child.stdin?.write(`\n`);
print('Waiting for app create script to be done');
await waitForExit(child);
@@ -205,7 +205,7 @@ async function createApp(appName, isPostgres, workspaceDir, rootDir) {
/**
* This points dependency resolutions into the workspace for each package that is present there
*/
async function overrideModuleResolutions(appDir, workspaceDir) {
async function overrideModuleResolutions(appDir: string, workspaceDir: string) {
const pkgJsonPath = resolvePath(appDir, 'package.json');
const pkgJson = await fs.readJson(pkgJsonPath);
@@ -231,19 +231,19 @@ async function overrideModuleResolutions(appDir, workspaceDir) {
/**
* Uses create-plugin command to create a new plugin in the app
*/
async function createPlugin(pluginName, appDir) {
async function createPlugin(pluginName: string, appDir: string) {
const child = spawnPiped(['yarn', 'create-plugin'], {
cwd: appDir,
});
try {
let stdout = '';
child.stdout.on('data', data => {
child.stdout?.on('data', (data: Buffer) => {
stdout = stdout + data.toString('utf8');
});
await waitFor(() => stdout.includes('Enter an ID for the plugin'));
child.stdin.write(`${pluginName}\n`);
child.stdin?.write(`${pluginName}\n`);
// await waitFor(() => stdout.includes('Enter the owner(s) of the plugin'));
// child.stdin.write('@someuser\n');
@@ -266,7 +266,7 @@ async function createPlugin(pluginName, appDir) {
/**
* Start serving the newly created app and make sure that the create plugin is rendering correctly
*/
async function testAppServe(pluginName, appDir) {
async function testAppServe(pluginName: string, appDir: string) {
const startApp = spawnPiped(['yarn', 'start'], {
cwd: appDir,
});
@@ -302,7 +302,7 @@ async function testAppServe(pluginName, appDir) {
}
/** Creates PG databases (drops if exists before) */
async function createDB(database) {
async function createDB(database: string) {
const config = {
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
@@ -321,7 +321,7 @@ async function createDB(database) {
/**
* Start serving the newly created backend and make sure that all db migrations works correctly
*/
async function testBackendStart(appDir, isPostgres) {
async function testBackendStart(appDir: string, isPostgres: boolean) {
if (isPostgres) {
print('Creating DBs');
await Promise.all(
@@ -343,10 +343,10 @@ async function testBackendStart(appDir, isPostgres) {
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
child.stdout?.on('data', (data: Buffer) => {
stdout = stdout + data.toString('utf8');
});
child.stderr.on('data', data => {
child.stderr?.on('data', (data: Buffer) => {
stderr = stderr + data.toString('utf8');
});
let successful = false;
@@ -384,4 +384,4 @@ async function testBackendStart(appDir, isPostgres) {
}
process.on('unhandledRejection', handleError);
main(process.argv.slice(2)).catch(handleError);
main().catch(handleError);
+33
View File
@@ -0,0 +1,33 @@
/*
* 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 { waitFor } from './helpers';
describe('waitFor', () => {
it('should wait for true', async () => {
const fn = jest.fn().mockReturnValue(true);
await waitFor(fn);
expect(fn).toHaveBeenCalledTimes(1);
});
it('should time out', async () => {
const fn = jest.fn().mockReturnValue(false);
await expect(waitFor(fn, 1)).rejects.toThrow(
'Timed out while waiting for condition',
);
expect(fn).toHaveBeenCalled();
});
});
@@ -14,16 +14,21 @@
* limitations under the License.
*/
const { spawn, execFile: execFileCb } = require('child_process');
const { promisify } = require('util');
import {
spawn,
execFile as execFileCb,
SpawnOptions,
ChildProcess,
} from 'child_process';
import { promisify } from 'util';
const execFile = promisify(execFileCb);
const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/;
function spawnPiped(cmd, options) {
function pipeWithPrefix(stream, prefix = '') {
return data => {
export function spawnPiped(cmd: string[], options?: SpawnOptions) {
function pipeWithPrefix(stream: NodeJS.WriteStream, prefix = '') {
return (data: Buffer) => {
const prefixedMsg = data
.toString('utf8')
.trimRight()
@@ -40,11 +45,11 @@ function spawnPiped(cmd, options) {
child.on('error', handleError);
const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' ');
child.stdout.on(
child.stdout?.on(
'data',
pipeWithPrefix(process.stdout, `[${logPrefix}].out: `),
);
child.stderr.on(
child.stderr?.on(
'data',
pipeWithPrefix(process.stderr, `[${logPrefix}].err: `),
);
@@ -52,7 +57,7 @@ function spawnPiped(cmd, options) {
return child;
}
async function runPlain(cmd, options) {
export async function runPlain(cmd: string[], options?: SpawnOptions) {
try {
const { stdout } = await execFile(cmd[0], cmd.slice(1), {
...options,
@@ -70,8 +75,9 @@ async function runPlain(cmd, options) {
}
}
function handleError(err) {
export function handleError(err: Error & { code?: unknown }) {
process.stdout.write(`${err.name}: ${err.stack || err.message}\n`);
if (typeof err.code === 'number') {
process.exit(err.code);
} else {
@@ -85,9 +91,14 @@ function handleError(err) {
* .cancel() is available
* @returns {Promise} Promise of resolution
*/
function waitFor(fn) {
return new Promise(resolve => {
export function waitFor(fn: () => boolean, maxSeconds: number = 120) {
let count = 0;
return new Promise((resolve, reject) => {
const handle = setInterval(() => {
if (count++ > maxSeconds * 10) {
reject(new Error('Timed out while waiting for condition'));
return;
}
if (fn()) {
clearInterval(handle);
resolve();
@@ -97,7 +108,7 @@ function waitFor(fn) {
});
}
async function waitForExit(child) {
export async function waitForExit(child: ChildProcess) {
if (child.exitCode !== null) {
throw new Error(`Child already exited with code ${child.exitCode}`);
}
@@ -113,10 +124,10 @@ async function waitForExit(child) {
);
}
async function waitForPageWithText(
browser,
path,
text,
export async function waitForPageWithText(
browser: any,
path: string,
text: string,
{ intervalMs = 1000, maxLoadAttempts = 240, maxFindTextAttempts = 3 } = {},
) {
let loadAttempts = 0;
@@ -163,16 +174,6 @@ async function waitForPageWithText(
}
}
function print(msg) {
export function print(msg: string) {
return process.stdout.write(`${msg}\n`);
}
module.exports = {
spawnPiped,
runPlain,
handleError,
waitFor,
waitForExit,
waitForPageWithText,
print,
};
@@ -14,16 +14,12 @@
* limitations under the License.
*/
module.exports = {
rules: {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
},
],
require('ts-node').register({
transpileOnly: true,
project: require('path').resolve(__dirname, '../../../tsconfig.json'),
compilerOptions: {
module: 'CommonJS',
},
};
});
require('./e2e-test');
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
declare module 'zombie';
declare module 'pgtools';
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
"@backstage/theme": "^0.1.1-alpha.18"
"@backstage/theme": "^0.1.1-alpha.19"
},
"devDependencies": {
"@storybook/addon-actions": "^5.3.17",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public"
@@ -44,7 +44,7 @@
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"commander": "^5.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/core-api": "^0.1.1-alpha.18",
"@backstage/test-utils-core": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/core-api": "^0.1.1-alpha.19",
"@backstage/test-utils-core": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.9.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18"
"@backstage/cli": "^0.1.1-alpha.19"
},
"files": [
"dist"
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/plugin-catalog": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.19",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/plugin-catalog": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@kyma-project/asyncapi-react": "^0.11.0",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.9.1",
@@ -37,9 +37,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/dev-utils": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/dev-utils": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -37,6 +37,7 @@ const columns: TableColumn<Entity>[] = [
.filter(Boolean)
.join(':'),
kind: entity.kind,
selectedTabId: 'overview',
})}
>
{entity.metadata.name}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/backend-common": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@types/express": "^4.17.6",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
@@ -50,7 +50,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
@@ -74,7 +74,7 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
idToken: params.id_token,
};
// Github provides an id numeric value (123)
// GitHub provides an id numeric value (123)
// as a fallback
const id = passportProfile!.id;
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -22,9 +22,9 @@
"mock-data:local": "./scripts/mock-data-local.sh"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@backstage/backend-common": "^0.1.1-alpha.19",
"@backstage/catalog-model": "^0.1.1-alpha.19",
"@backstage/config": "^0.1.1-alpha.19",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -41,7 +41,7 @@
"yup": "^0.29.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@types/lodash": "^4.14.151",
"@types/node-fetch": "^2.5.7",
"@types/supertest": "^2.0.8",
@@ -50,7 +50,7 @@ describe('GitlabApiReaderProcessor', () => {
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/',
url: null,
err:
'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: Gitlab url does not end in .ya?ml',
'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: GitLab url does not end in .ya?ml',
},
];
@@ -77,7 +77,7 @@ export class GitlabApiReaderProcessor implements LocationProcessor {
const branchAndfilePath = url.pathname.split('/-/blob/')[1];
if (!branchAndfilePath.match(/\.ya?ml$/)) {
throw new Error('Gitlab url does not end in .ya?ml');
throw new Error('GitLab url does not end in .ya?ml');
}
const [branch, ...filePath] = branchAndfilePath.split('/');
@@ -127,7 +127,7 @@ export class GitlabApiReaderProcessor implements LocationProcessor {
return projectID;
} catch (e) {
throw new Error(`Could not get Gitlab ProjectID for: ${target}, ${e}`);
throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`);
}
}
}
@@ -77,7 +77,7 @@ export class GitlabReaderProcessor implements LocationProcessor {
blobKeyword !== 'blob' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong Gitlab URL');
throw new Error('Wrong GitLab URL');
}
// Replace 'blob' with 'raw'
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,15 +21,15 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/plugin-api-docs": "^0.1.1-alpha.18",
"@backstage/plugin-github-actions": "^0.1.1-alpha.18",
"@backstage/plugin-jenkins": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.18",
"@backstage/plugin-sentry": "^0.1.1-alpha.18",
"@backstage/plugin-techdocs": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.19",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/plugin-api-docs": "^0.1.1-alpha.19",
"@backstage/plugin-github-actions": "^0.1.1-alpha.19",
"@backstage/plugin-jenkins": "^0.1.1-alpha.19",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.19",
"@backstage/plugin-sentry": "^0.1.1-alpha.19",
"@backstage/plugin-techdocs": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -42,9 +42,9 @@
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/dev-utils": "^0.1.1-alpha.18",
"@backstage/test-utils": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/dev-utils": "^0.1.1-alpha.19",
"@backstage/test-utils": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-circleci",
"version": "0.1.1-alpha.18",
"version": "0.1.1-alpha.19",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
"postpack": "backstage-cli postpack"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.19",
"@backstage/theme": "^0.1.1-alpha.19",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -36,8 +36,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/dev-utils": "^0.1.1-alpha.18",
"@backstage/cli": "^0.1.1-alpha.19",
"@backstage/dev-utils": "^0.1.1-alpha.19",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",

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