This commit is contained in:
Tim Hansen
2020-08-24 14:11:11 -06:00
39 changed files with 587 additions and 241 deletions
@@ -1,11 +1,11 @@
name: Frontend CI
name: CI
on:
pull_request:
paths-ignore:
- 'microsite/**'
jobs:
build:
verify:
runs-on: ubuntu-latest
strategy:
@@ -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:
@@ -42,6 +43,7 @@ jobs:
- name: yarn install
run: yarn install --frozen-lockfile
- 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 }}
@@ -51,13 +50,14 @@ jobs:
- name: yarn install
run: yarn install --frozen-lockfile
- 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
+47
View File
@@ -0,0 +1,47 @@
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
- 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') }}
- 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: yarn install
run: yarn install --frozen-lockfile
# Tests are broken on Windows, disabled for now
# - name: test
# run: yarn lerna -- run test
+1 -1
View File
@@ -1,4 +1,4 @@
name: Master Build
name: Main Master Build
on:
push:
+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 |
-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: 101 KiB

@@ -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
@@ -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
+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)
+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.
+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"
+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",
+1 -1
View File
@@ -35,7 +35,7 @@ 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',
},
{
+1
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'
+4 -1
View File
@@ -37,7 +37,10 @@ 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'),
'\\.(js|jsx|ts|tsx)$': [
require.resolve('ts-jest'),
{ isolatedModules: true },
],
'\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve(
'./jestFileTransform.js',
),
+2 -7
View File
@@ -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 --"
},
@@ -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",
+5
View File
@@ -198,6 +198,11 @@ export function createBackendConfig(
chunkFilename: isDev
? '[name].chunk.js'
: '[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]',
}
: {}),
},
plugins: [
new StartServerPlugin('main.js'),
+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';
@@ -5,6 +5,7 @@ metadata:
description: backstage.io
annotations:
github.com/project-slug: 'spotify/backstage'
backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
spec:
type: website
lifecycle: production
@@ -5,9 +5,9 @@ metadata:
title: Create React App Template
description: Create a new CRA website project
tags:
- Experimental
- React
- CRA
- experimental
- react
- cra
spec:
owner: web@example.com
templater: cra
@@ -5,8 +5,8 @@ metadata:
title: React SSR Template
description: Create a website powered with Next.js
tags:
- Recommended
- React
- recommended
- react
spec:
owner: web@example.com
templater: cookiecutter
@@ -5,8 +5,8 @@ metadata:
title: Spring Boot GRPC Service
description: Create a simple microservice using gRPC and Spring Boot Java
tags:
- Recommended
- Java
- recommended
- java
spec:
owner: service@example.com
templater: cookiecutter
+4 -4
View File
@@ -20008,10 +20008,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rollup-plugin-dts@^1.4.6:
version "1.4.10"
resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.10.tgz#0373c4284a2ba4d2d72df69c289271a816bc2736"
integrity sha512-bL6MBXc8lK7D5b/tYbHaglxs4ZxMQTQilGA6Xm9KQBEj4h9ZwIDlAsvDooGjJ/cOw23r3POTRtSCEyTHxtzHJg==
rollup-plugin-dts@1.4.11:
version "1.4.11"
resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.11.tgz#aedf0b7bb91d51e20b755e2c18e840edfc7af7a1"
integrity sha512-yiScAMKgwH77b44a/IFGgjLsmwSlNfQhEM+eCb2uMrupQMPE1n/12wrnT431+v1u6wYMF1XuHqldh+v/7mTvYA==
optionalDependencies:
"@babel/code-frame" "^7.10.4"