Merge branch 'master' of https://github.com/spotify/backstage
This commit is contained in:
@@ -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
|
||||
@@ -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,4 +1,4 @@
|
||||
name: Master Build
|
||||
name: Main Master Build
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
+14
-13
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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,6 +1,6 @@
|
||||
---
|
||||
id: index
|
||||
title: Intro
|
||||
title: Intro to plugins
|
||||
---
|
||||
|
||||
Backstage is a single-page application composed of a set of plugins.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"overview/architecture-overview",
|
||||
"overview/architecture-terminology",
|
||||
"overview/roadmap",
|
||||
"overview/vision"
|
||||
"overview/vision",
|
||||
"overview/adopting"
|
||||
],
|
||||
"Getting Started": [
|
||||
"getting-started/index",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
Vendored
+18
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user