diff --git a/.changeset/brave-zoos-fail.md b/.changeset/brave-zoos-fail.md deleted file mode 100644 index 3d24215cac..0000000000 --- a/.changeset/brave-zoos-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins': patch ---- - -Avoid loading data from Jenkins twice. Don't load data when navigating through the pages as all data from all pages is already loaded. diff --git a/.changeset/calm-icons-jump.md b/.changeset/calm-icons-jump.md deleted file mode 100644 index 16054a80e6..0000000000 --- a/.changeset/calm-icons-jump.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/core-api': patch -'@backstage/dev-utils': patch ---- - -Update ApiFactory type to correctly infer API type and disallow mismatched implementations. - -This fixes for example the following code: - -```ts -interface MyApi { - myMethod(): void -} - -const myApiRef = createApiRef({...}); - -createApiFactory({ - api: myApiRef, - deps: {}, - // This should've caused an error, since the empty object does not fully implement MyApi - factory: () => ({}), -}) -``` diff --git a/.changeset/chatty-pens-bathe.md b/.changeset/chatty-pens-bathe.md deleted file mode 100644 index 4ca3378b2e..0000000000 --- a/.changeset/chatty-pens-bathe.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -'@backstage/plugin-sentry': minor -'@backstage/plugin-sentry-backend': minor ---- - -The plugin uses the `proxy-backend` instead of a custom `sentry-backend`. -It requires a proxy configuration: - -`app-config.yaml`: - -```yaml -proxy: - '/sentry/api': - target: https://sentry.io/api/ - allowedMethods: ['GET'] - headers: - Authorization: - $env: SENTRY_TOKEN # export SENTRY_TOKEN="Bearer " -``` - -The `MockApiBackend` is no longer configured by the `NODE_ENV` variable. -Instead, the mock backend can be used with an api-override: - -`packages/app/src/apis.ts`: - -```ts -import { createApiFactory } from '@backstage/core'; -import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry'; - -export const apis = [ - // ... - - createApiFactory(sentryApiRef, new MockSentryApi()), -]; -``` - -If you already use the Sentry backend, you must remove it from the backend: - -Delete `packages/backend/src/plugins/sentry.ts`. - -```diff -# packages/backend/package.json - -... - "@backstage/plugin-scaffolder-backend": "^0.3.2", -- "@backstage/plugin-sentry-backend": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.3.0", -... -``` - -```diff -// packages/backend/src/index.html - - const apiRouter = Router(); - apiRouter.use('/catalog', await catalog(catalogEnv)); - apiRouter.use('/rollbar', await rollbar(rollbarEnv)); - apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); -- apiRouter.use('/sentry', await sentry(sentryEnv)); - apiRouter.use('/auth', await auth(authEnv)); - apiRouter.use('/techdocs', await techdocs(techdocsEnv)); - apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); - apiRouter.use('/proxy', await proxy(proxyEnv)); - apiRouter.use('/graphql', await graphql(graphqlEnv)); - apiRouter.use(notFoundHandler()); -``` diff --git a/.changeset/cost-insights-quick-lizards-smash.md b/.changeset/cost-insights-quick-lizards-smash.md deleted file mode 100644 index eae60fc744..0000000000 --- a/.changeset/cost-insights-quick-lizards-smash.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-cost-insights': minor ---- - -Add support for multiple types of entity cost breakdown. - -This change is backwards-incompatible with plugin-cost-insights 0.3.x; the `entities` field on Entity returned in product cost queries changed from `Entity[]` to `Record prom.register.clear()); - - return promBundle({ - includeMethod: true, - includePath: true, - // Using includePath alone is problematic, as it will include path labels with high - // cardinality (e.g. path params). Instead we would have to template them. However, this - // is difficult, as every backend plugin might use different routes. Instead we only take - // the first directory of the path, to have at least an idea how each plugin performs: - normalizePath, - promClient: { collectDefaultMetrics: {} }, - }); -} -``` - -Last, extend your router configuration with the `metricsHandler`: - -```diff -+import { metricsHandler } from './metrics'; - -... - - const service = createServiceBuilder(module) - .loadConfig(config) - .addRouter('', await healthcheck(healthcheckEnv)) -+ .addRouter('', metricsHandler()) - .addRouter('/api', apiRouter); -``` - -Your Prometheus metrics will be available at the `/metrics` endpoint. diff --git a/.changeset/red-worms-fold.md b/.changeset/red-worms-fold.md deleted file mode 100644 index 41dcc75c4c..0000000000 --- a/.changeset/red-worms-fold.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/core-api': patch ---- - -Deprecated the `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types and added a new `RouteRef` type as replacement. - -Deprecated and disabled the `createSubRoute` method of `AbsoluteRouteRef`. - -Add an as of yet unused `params` option to `createRouteRef`. diff --git a/.changeset/seven-tips-begin.md b/.changeset/seven-tips-begin.md deleted file mode 100644 index 5609315144..0000000000 --- a/.changeset/seven-tips-begin.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-api-docs': minor ---- - -Stop exposing a custom router from the `api-docs` plugin. Instead, use the -widgets exported by the plugin to compose your custom entity pages. - -Instead of displaying the API definitions directly in the API tab of the -component, it now contains tables linking to the API entities. This also adds -new widgets to display relationships (bot provides & consumes relationships) -between components and APIs. - -See the changelog of `create-app` for a migration guide. diff --git a/.changeset/seven-tips-more.md b/.changeset/seven-tips-more.md deleted file mode 100644 index 9a6c4855c3..0000000000 --- a/.changeset/seven-tips-more.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Adjust template to the latest changes in the `api-docs` plugin. - -## Template Changes - -While updating to the latest `api-docs` plugin, the following changes are -necessary for the `create-app` template in your -`app/src/components/catalog/EntityPage.tsx`. This adds: - -- A custom entity page for API entities -- Changes the API tab to include the new `ConsumedApisCard` and - `ProvidedApisCard` that link to the API entity. - -```diff - import { -+ ApiDefinitionCard, -- Router as ApiDocsRouter, -+ ConsumedApisCard, -+ ProvidedApisCard, -+ ConsumedApisCard, -+ ConsumingComponentsCard, -+ ProvidedApisCard, -+ ProvidingComponentsCard - } from '@backstage/plugin-api-docs'; - -... - -+const ComponentApisContent = ({ entity }: { entity: Entity }) => ( -+ -+ -+ -+ -+ -+ -+ -+ -+); - - const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( - - } - /> - } - /> - } -+ element={} - /> -... - --export const EntityPage = () => { -- const { entity } = useEntity(); -- switch (entity?.spec?.type) { -- case 'service': -- return ; -- case 'website': -- return ; -- default: -- return ; -- } --}; - -+export const ComponentEntityPage = ({ entity }: { entity: Entity }) => { -+ switch (entity?.spec?.type) { -+ case 'service': -+ return ; -+ case 'website': -+ return ; -+ default: -+ return ; -+ } -+}; -+ -+const ApiOverviewContent = ({ entity }: { entity: Entity }) => ( -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+); -+ -+const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => ( -+ -+ -+ -+ -+ -+); -+ -+const ApiEntityPage = ({ entity }: { entity: Entity }) => ( -+ -+ } -+ /> -+ } -+ /> -+ -+); -+ -+export const EntityPage = () => { -+ const { entity } = useEntity(); -+ -+ switch (entity?.kind?.toLowerCase()) { -+ case 'component': -+ return ; -+ case 'api': -+ return ; -+ default: -+ return ; -+ } -+}; -``` diff --git a/.changeset/shaggy-camels-remain.md b/.changeset/shaggy-camels-remain.md deleted file mode 100644 index f1c4e17643..0000000000 --- a/.changeset/shaggy-camels-remain.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'example-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/create-app': patch ---- - -Unify `dockerode` library and type dependency versions diff --git a/.changeset/silent-boxes-exercise.md b/.changeset/silent-boxes-exercise.md deleted file mode 100644 index 0d2003cec9..0000000000 --- a/.changeset/silent-boxes-exercise.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/catalog-model': minor -'@backstage/plugin-catalog-backend': minor ---- - -Remove the deprecated fields `ancestors` and `descendants` from the `Group` entity. - -See https://github.com/backstage/backstage/issues/3049 and the PRs linked from it for details. diff --git a/.changeset/silly-kiwis-rest.md b/.changeset/silly-kiwis-rest.md deleted file mode 100644 index acb0d79bb2..0000000000 --- a/.changeset/silly-kiwis-rest.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -Removed modifyCss transformer and moved the css to injectCss transformer -Fixed issue where some internal doc links would cause a reload of the page diff --git a/.changeset/small-comics-cover.md b/.changeset/small-comics-cover.md deleted file mode 100644 index 90fa15593d..0000000000 --- a/.changeset/small-comics-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/theme': patch ---- - -Add a little more padding in dense tables diff --git a/.changeset/tall-hairs-switch.md b/.changeset/tall-hairs-switch.md deleted file mode 100644 index 2a2f915a6c..0000000000 --- a/.changeset/tall-hairs-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Export the `defaultConfigLoader` implementation diff --git a/.changeset/unlucky-kiwis-rescue.md b/.changeset/unlucky-kiwis-rescue.md deleted file mode 100644 index 71afaeb44a..0000000000 --- a/.changeset/unlucky-kiwis-rescue.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/core': minor ---- - -Introducing a new optional property within `app-config.yaml` called `auth.environment` to have configurable environment value for `auth.providers` - -**Default Value:** 'development' - -**Optional Values:** 'production' | 'development' - -**Migration-steps:** - -- To override the default value, one could simply introduce the new property `environment` within the `auth` section of the `config.yaml` -- re-run the build to reflect the changed configs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b3c9bc653d..495f1ff78d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ /docs/features/techdocs @backstage/techdocs-core /plugins/cost-insights @backstage/silver-lining /plugins/cloudbuild @trivago/ebarrios -/plugins/techdocs @backstage/techdocs-core /plugins/search @backstage/techdocs-core +/plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core +/packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index b595c4ba2b..b4be37c057 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -1,4 +1,5 @@ abc +adamdmharvey andrewthauer Apdex api @@ -23,6 +24,7 @@ changesets Changesets chanwit Chanwit +ci cisphobia cissexist classname @@ -34,6 +36,7 @@ Codecov codehilite Codehilite codeowners +composable config Config configmaps @@ -42,10 +45,12 @@ const cookiecutter css dariddler +dataflow deadnaming destructured dev devs +dhenneke discoverability Discoverability dls @@ -54,6 +59,8 @@ Dockerfile Dockerize dockerode Docusaurus +Dominik +dtuite dzolotusky eg Ek @@ -64,6 +71,7 @@ facto failover Figma Firekube +Fiverr freben Fredrik github @@ -76,6 +84,7 @@ graphviz Gustavsson Hackathons haproxy +Henneke heroku Heroku horizontalpodautoscalers @@ -137,14 +146,17 @@ Oldsberg onboarding Onboarding pagerduty +parallelization Patrik Phoen plantuml Pomaceous postgres +postpack pre prebaked preconfigured +prepack Preprarer Prerequisities productional @@ -182,10 +194,12 @@ Serverless Sinon smartsymobls Snyk +sourcemaps sparklines Spotifiers spotify Spotify +sqlite squidfunk src stefanalund @@ -214,6 +228,9 @@ toolsets tooltip tooltips touchpoints +transpiled +transpilation +Tuite ui untracked upvote diff --git a/ADOPTERS.md b/ADOPTERS.md index b6455e09b0..3d5914d97b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,16 +1,18 @@ -| 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 | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| Organization | Contact | Description of Use | +| -------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | 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 | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | diff --git a/OWNERS.md b/OWNERS.md index 8e5002fc0a..f1649c50b8 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -22,3 +22,6 @@ People that have made significant contributions to the project and earned write - Andrew Thauer - Wealthsimple (GitHub: [andrewthauer](https://github.com/andrewthauer)) - Oliver Sand - SDA SE (GitHub: [Fox32](https://github.com/Fox32)) +- David Tuite - Roadie (GitHub: [dtuite](https://github.com/dtuite)) +- Adam Harvey - DXC Technology (GitHub: [adamdmharvey](https://github.com/adamdmharvey)) +- Dominik Henneke - SDA SE (GitHub: [dhenneke](https://github.com/dhenneke)) diff --git a/app-config.yaml b/app-config.yaml index 9d4d702473..f6f71478d0 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -68,11 +68,15 @@ proxy: organization: name: My Company +# Reference documentation http://backstage.io/docs/features/techdocs/configuration techdocs: - storageUrl: http://localhost:7000/api/techdocs/static/docs requestUrl: http://localhost:7000/api/techdocs + storageUrl: http://localhost:7000/api/techdocs/static/docs + builder: 'local' # Alternatives - 'external' generators: - techdocs: 'docker' + techdocs: 'docker' # Alternatives - 'local' + publisher: + type: 'local' # Alternatives - 'googleGcs'. Read documentation for using alternatives. sentry: organization: my-company @@ -133,7 +137,7 @@ catalog: $env: GITHUB_TOKEN #### Example for how to add your GitHub Enterprise instance using the API: # - target: https://ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 + # apiBaseUrl: https://ghe.example.net/api # token: # $env: GHE_TOKEN ldapOrg: diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md index 5d633544e3..9b5d77bc7c 100644 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md @@ -3,7 +3,7 @@ ExampleComponent.tsx reference ```tsx -import React, { FC } from 'react'; +import React from 'react'; import { Typography, Grid } from '@material-ui/core'; import { InfoCard, @@ -18,7 +18,7 @@ import { import { useApi } from '@backstage/core-api'; import ExampleFetchComponent from '../ExampleFetchComponent'; -const ExampleComponent: FC<{}> = () => { +const ExampleComponent = () => { const identityApi = useApi(identityApiRef); const userId = identityApi.getUserId(); const profile = identityApi.getProfile(); diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md index 08d14e8c4c..6061b69e93 100644 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleFetchComponent.md @@ -3,7 +3,7 @@ ExampleFetchComponent.tsx reference ```tsx -import React, { FC } from 'react'; +import React from 'react'; import { useAsync } from 'react-use'; import Alert from '@material-ui/lab/Alert'; import { @@ -57,7 +57,7 @@ type DenseTableProps = { viewer: Viewer; }; -export const DenseTable: FC = ({ viewer }) => { +export const DenseTable = ({ viewer }: DenseTableProps) => { const columns: TableColumn[] = [ { title: 'Name', field: 'name' }, { title: 'Created', field: 'createdAt' }, @@ -76,7 +76,7 @@ export const DenseTable: FC = ({ viewer }) => { ); }; -const ExampleFetchComponent: FC<{}> = () => { +const ExampleFetchComponent = () => { const auth = useApi(githubAuthApiRef); const { value, loading, error } = useAsync(async (): Promise => { diff --git a/docs/FAQ.md b/docs/FAQ.md index d8be258141..da10f9724c 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -152,6 +152,27 @@ plugins to share dependencies between each other when possible. This contributes to Backstage being fast, which is an important part of the user and developer experience. +### Why are there no published Docker images or helm charts for Backstage? + +As mentioned above, Backstage is not a packaged service that you can use out of +the box. In order to get started with Backstage you need to use the +`@backstage/create-app` package to create and customize your own Backstage app. + +In order to build a Docker image from your own app, you can use the +`yarn build-image` command which is included out of the box in the app template. +By default this image will bundle up both the frontend and the backend into a +single image that you can deploy using your favorite tooling. + +There are also some examples that can help you deploy Backstage to kubernetes in +the +[contrib](https://github.com/backstage/backstage/tree/master/contrib/kubernetes) +folder. + +It is possible that example images will be provided in the future, which can be +used to quickly try out a small subset of the functionality of Backstage, but +these would not be able to provide much more functionality on top of what you +can see on a demo site. + ### Do I have to write plugins in TypeScript? No, you can use JavaScript if you prefer. We want to keep the Backstage core diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 2547db5c5c..ff322c136f 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -33,10 +33,10 @@ hook exported by `@backstage/core`, or the `withApis` HOC if you prefer class components. For example, the `ErrorApi` can be accessed like this: ```tsx -import React, { FC } from 'react'; +import React from 'react'; import { useApi, errorApiRef } from '@backstage/core'; -export const MyComponent: FC<{}> = () => { +export const MyComponent = () => { const errorApi = useApi(errorApiRef); // Signal to the app that something went wrong, and display the error to the user. diff --git a/docs/cli/commands.md b/docs/cli/commands.md new file mode 100644 index 0000000000..c505bf8ae7 --- /dev/null +++ b/docs/cli/commands.md @@ -0,0 +1,559 @@ +--- +id: commands +title: Commands +description: Descriptions of all commands available in the CLI. +--- + +This page lists all commands provided by the Backstage CLI, what they're for, +and where to use them. + +The documentation for each command begins with specifying its scope, this +indicates where the command should be used by selecting from the following list: + +- `app` - A frontend app package, such as `packages/app`. +- `backend` - A backend package, such as `packages/backend`. +- `frontend-plugin` - A frontend plugin package. +- `backend-plugin` - A backend plugin package. +- `root` - The monorepo root. +- `any` - Any kind of package, but not the repo root. + +## help + +This command displays a help summary or detailed help screens for each command. +Below is a cleaned up output of `yarn backstage-cli --help`. + +```text +app:build Build an app for a production release +app:diff Diff an existing app with the creation template +app:serve Serve an app for local development + +backend:build Build a backend plugin +backend:build-image Bundles the package into a docker image +backend:dev Start local development server with HMR for the backend + +plugin:build Build a plugin +plugin:diff Diff an existing plugin with the creation template +plugin:serve Serves the dev/ folder of a plugin + +build Build a package for publishing +build-workspace Builds a temporary dist workspace from the provided packages +lint Lint a package +test Run tests, forwarding args to Jest, defaulting to watch mode +clean Delete cache directories + +create-plugin Creates a new plugin in the current repository +remove-plugin Removes plugin in the current repository + +config:print Print the app configuration for the current package +config:check Validate that the given configuration loads and matches schema + +versions:bump Bump Backstage packages to the latest versions +versions:check Check Backstage package versioning + +prepack Prepares a package for packaging before publishing +postpack Restores the changes made by the prepack command + +help [command] display help for command +``` + +## app:build + +Scope: `app` + +Builds a bundle of static content from the app, which can then be served via any +static web server such as `nginx`, or via the +[`app-backend`](https://www.npmjs.com/package/@backstage/plugin-app-backend) +plugin directly from a Backstage backend instance. + +The command also reads and injects static configuration into the bundle. It is +important to note that when deploying using your own static content hosting +solution, this will be the final configuration used in the frontend unless you +for example hook in configuration loading from the backend. When using the +`nginx` based Dockerfile in this repo along with its included run script, +`APP_CONFIG_` environment variables will be injected into the frontend, and when +serving using the `app-backend` plugin, the configuration is completely injected +from the backend and the configuration at the time of calling this command will +not be used. + +Note that even when injecting configuration at runtime, it is not possible to +change the base path of the app. For example, if you at build time have +`app.baseUrl` set to `http://dev-app.com/my-app`, you can change that to +`https://prod-app.com/my-app`, but not to `https://prod-app.com`, as that would +change the path. + +During the build, the following variables are set: + +```java +process.env.NODE_ENV = 'production'; +process.env.BUILD_INFO = { + cliVersion: '0.4.0', // The version of the CLI package + gitVersion: 'v0.4.0-86-ge54815618', // output of `git describe --always` + packageVersion: '1.0.5', // The version of the app package itself + timestamp: 1678900000000, // Date.now() when the build started + commit: 'e548156182a973ed4b459e18533afc22c85ffff8', // output of `git rev-parse HEAD` +}; +``` + +Some CI environments do not properly report correct resource limits, potentially +leading to errors such as `ENOMEM` during compilation. If you run into this +issue you can limit the parallelization of the build process by setting the +environment variable `BACKSTAGE_CLI_BUILD_PARALLEL`, which is forwarded to the +[`terser-webpack-plugin`](https://github.com/webpack-contrib/terser-webpack-plugin#parallel). +You can set it to `false` or `1` to completely disable parallelization, but +usually a low value such as `2` is enough. + +```text +Usage: backstage-cli app:build + +Options: + --stats Write bundle stats to output directory + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## app:diff + +Scope: `app` + +Diff an existing app with the template used in `@backstage/create-app`. This +will verify that your app package has not diverged from the template, and can be +useful to run after updating the version of `@backstage/cli` in your app. + +This command is experimental and may be removed in the future. + +```text +Usage: backstage-cli app:diff + +Options: + --check Fail if changes are required + --yes Apply all changes + -h, --help display help for command +``` + +## app:serve + +Scope: `app` + +Serve an app for local development. This starts up a local development server, +using a bundling configuration that is quite similar to that of the `app:build` +command, but with development features such as React Hot Module Replacement, +faster sourcemaps, no minification, etc. + +The static configuration is injected into the frontend, but it does not support +watching, meaning that changes in for example `app-config.yaml` are not +reflected until the serve process is restarted. + +During the build, the following variables are set: + +```java +process.env.NODE_ENV = 'development'; +process.env.BUILD_INFO = { /* See app:build */ }; +``` + +The server listening configuration is controlled through the static +configuration. The `app.baseUrl` determines the listening host and port, as well +as whether HTTPS is used or not. It is also possible to override the listening +host and port if needed by setting `app.listen.host` and `app.listen.port`. + +```text +Usage: backstage-cli app:serve [options] + +Options: + --check Enable type checking and linting + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## backend:build + +Scope: `backend`, `backend-plugin` + +This builds a backend package for publishing and use in production. The build +output is written to `dist/`. Be sure to list any additional file that the +package depends on at runtime in the `"files"` field inside `package.json`, a +common example being the `migrations` directory. + +```text +Usage: backstage-cli backend:build [options] + +Options: + -h, --help display help for command +``` + +## backend:build-image + +Scope: `backend` + +Builds a Docker image of the backend package, forwarding all unknown options to +`docker image build`. For example: + +```bash +yarn backstage-cli backend:build-image --build --tag my-backend-image +``` + +The image is built using the backend package along with all of its local package +dependencies. It expects to find a `Dockerfile` at the root of the backend +package, which will be used during the build. + +The Dockerfile is **NOT** executed within the package or repo itself. Because +the packages in the repo itself are configured for development instead of +production use, the final Docker build happens in a separate temporary +directory, to which the backend package and dependencies have been copied. Only +files listed within the `"files"` field within each package's `package.json` are +copied over, along with the root `package.json`, `yarn.lock`, and any +`app-config.*.yaml` files. + +During the build a `skeleton.tar` file is created and put at the repo root. This +file contains the `package.json` of each included package, which together with +the root `package.json` and `yarn.lock` can be used to run a cached +`yarn install` before the full production builds of all the packages are copied +over, providing a significant speedup if Docker build layer caching available. + +This command is experimental and we hope to be able to replace it with one that +is less integrated directly with Docker, and also supports multi-stage Docker +builds. It is possible to replicate most of what this command does by manually +building each package, and then use the `build-workspace` to create the +temporary workspace, and finally copy over any additional files to the workspace +and execute the Docker build within it. + +```text +Usage: backstage-cli backend:build-image [options] + +Options: + --build Build packages before packing them into the image + --backstage-cli-help display help for command +``` + +## backend:dev + +Scope: `backend`, `backend-plugin` + +Starts a backend package in development mode, with watch mode enabled for all +local dependencies. + +```text +Usage: backstage-cli backend:dev [options] + +Options: + --check Enable type checking and linting + --inspect Enable debugger + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## create-plugin + +Scope: `root` + +Creates a new plugin within the repository. This command is typically wrapped up +in the root `package.json` to be executed with `yarn create-plugin`, using +options that are appropriate for the organization that owns the app repo. A +recommended scope for internal packages is `@internal`. + +```text +Usage: backstage-cli create-plugin [options] + +Options: + --backend Create plugin with the backend dependencies as default + --scope <scope> npm scope + --npm-registry <URL> npm registry URL + --no-private Public npm package + -h, --help display help for command +``` + +## remove-plugin + +Scope: `root` + +A utility to remove a plugin from a repo, essentially undoing everything that +was done by `create-plugin`. + +This is primarily intended as a utility for manual tests and end to end testing +scripts. + +```text +Usage: backstage-cli remove-plugin [options] + +Options: + -h, --help display help for command +``` + +## plugin:build + +Scope: `frontend-plugin` + +Build a frontend plugin for publishing to a package registry. There is no need +to run this command during development or even in CI unless the package is being +published. The `app:bundle` command does not use the output for this command +when bundling local package dependencies. + +The output is written to a `dist/` folder. It also outputs type declarations for +the plugin, and therefore requires `yarn tsc` to have been run first. The input +type declarations are expected to be found within `dist-types/` at the root of +the monorepo. + +```text +Usage: backstage-cli plugin:build [options] + +Options: + -h, --help display help for command +``` + +## plugin:serve + +Scope: `frontend-plugin` + +Serves a frontend plugin by itself for isolated development. The serve task +itself is essentially identical to `app:serve`, but the entrypoint is instead +set to the `dev/` folder within the plugin. + +The `dev/` folder typically contains a small wrapper script that hooks up any +necessary mock APIs or other things that are needed for the plugin to function. +The `@backstage/dev-utils` package provides utilities to that end. + +```text +Usage: backstage-cli plugin:serve [options] + +Options: + --check Enable type checking and linting + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## plugin:diff + +Scope: `frontend-plugin` + +Compares a frontend plugin to the `create-plugin` template, making sure that it +hasn't diverged from the template and recommending updates when it has. A good +practice is to run this command after updating the version of the CLI in a +project. + +```text +Usage: backstage-cli plugin:diff [options] + +Options: + --check Fail if changes are required + --yes Apply all changes + -h, --help display help for command +``` + +## build + +Scope: `any` + +Build a single package for publishing, just like the `plugin:build` and +`backend:build` commands. This command is intended for standalone packages that +aren't plugins, and for example support building of isomorphic packages for +usage in both the frontend and backend. + +For frontend packages you'll want to include `esm` output, and for backend +packages `cjs`. Whether to include `types` depends on if you need type +declarations for the package, and also requires `yarn tsc` to have been run +first. + +```text +Usage: backstage-cli build [options] + +Options: + --outputs <formats> List of formats to output [types,cjs,esm] + -h, --help display help for command +``` + +## lint + +Scope: `any` + +Lint a package. In addition to the default `eslint` behavior, this command will +include TypeScript files, treat warnings as errors, and default to linting the +entire directory if no specific files are listed. + +```text +Usage: backstage-cli lint [options] + +Options: + --format <format> Lint report output format (default: "eslint-formatter-friendly") + --fix Attempt to automatically fix violations + -h, --help display help for command +``` + +## test + +Scope: `any` + +Run tests, forwarding all unknown options to Jest, and defaulting to watch mode. +When executing the tests, `process.env.NODE_ENV` will be set to `"test"`. + +This command uses a default Jest configuration that is included in the CLI, +which is set up with similar goals for speed, scale, and working within a +monorepo. The configuration sets the `src` as the root directory, enforces the +`.test.` infix for tests, and uses `src/setupTests.ts` as the test setup +location. + +If needed, the configuration can be extended using a `"jest"` field in +`package.json`, both within the target package and the monorepo root, with +configuration in the target package taking precedence. Refer to the +[Jest configuration documentation](https://jestjs.io/docs/en/configuration) for +a full list of configuration options. + +In addition to the Jest configuration there's an optional `transformModules` +option, which is an array of module names to include in transformations. +Normally modules inside `node_modules` are not transformed, but there are cases +were published packages are not transpiled far enough to be usable by Jest, in +which case you need to enable transform of them. + +Another way to override the Jest configuration is to place a `jest.config.js` or +`jest.config.ts` file in the package root. As opposed to the `package.json` way +of overriding config, this completely removes the base config, and so you need +to set it up from scratch. + +```text +Usage: backstage-cli test [options] + +Options: + --backstage-cli-help display help for command +``` + +## config:print + +Scope: `root` + +Print the static configuration, defaulting to reading `app-config.yaml` in the +repo root, using schema collected from all local packages in the repo. + +For example, to validate that a given configuration value is visible in the +frontend when building the `my-app` package, you can use the following: + +```bash +yarn backstage-cli config:print --frontend --package my-app +``` + +```text +Usage: backstage-cli config:print [options] + +Options: + --package <name> Only load config schema that applies to the given package + --frontend Print only the frontend configuration + --with-secrets Include secrets in the printed configuration + --format <format> Format to print the configuration in, either json or yaml [yaml] + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## config:check + +Scope: `root` + +Validate that static configuration loads and matches schema, defaulting to +reading `app-config.yaml` in the repo root and using schema collected from all +local packages in the repo. + +```text +Usage: backstage-cli config:check [options] + +Options: + --package <name> Only load config schema that applies to the given package + --config <path> Config files to load instead of app-config.yaml (default: []) + -h, --help display help for command +``` + +## versions:bump + +Scope: `root` + +Bump all `@backstage` packages to the latest versions. This checks for updates +in the package registry, and will update entries both in `yarn.lock` and +`package.json` files when necessary. + +```text +Usage: backstage-cli versions:bump [options] + +Options: + -h, --help display help for command +``` + +## versions:check + +Scope: `root` + +Validate `@backstage` dependencies within the repo, making sure that there are +no duplicates of packages that might lead to breakages. For example, +`@backstage/core` must not be loaded in twice, so having two different versions +of it installed will cause this command to exit with an error. + +By supplying the `--fix` flag the command will attempt to fix any conflict that +can be resolved by editing `yarn.lock`, but will not attempt to search for +remote updates or modify any `package.json` files. + +```text +Usage: backstage-cli versions:check [options] + +Options: + --fix Fix any auto-fixable versioning problems + -h, --help display help for command +``` + +## prepack + +Scope: `any` + +This command should be added as `scripts.prepack` in all packages. It enables +packaging- and publish-time overrides for fields inside `packages.json`. + +The checked in version of all packages in a Backstage monorepo are tailored for +local development, and as such `main` and similar fields inside `package.json` +point to development source, i.e. `src/index.ts`. Using this when publishing +would lead to a broken package, since `src/` is not included in the published +package and we instead need to point to files in the `dist/` directory. This +command allows for those fields to be rewritten when needed, and does so by +copying all fields within `publishConfig` to the top-level of each +`package.json`, skipping `access`, `registry`, and `tag`. + +The need for this command may be removed in the future, as this exact method of +overriding fields for publishing is already supported by some package managers. + +```text +Usage: backstage-cli prepack [options] + +Options: + -h, --help display help for command +``` + +## postpack + +Scope: `any` + +This should be added as `scripts.postpack` in all packages. It restores +`package.json` to what it looked like before calling the `prepack` command. + +```text +Usage: backstage-cli postpack [options] + +Options: + -h, --help display help for command +``` + +## clean + +Scope: `any` + +Remove cache and output directories. + +```text +Usage: backstage-cli clean [options] + +Options: + -h, --help display help for command +``` + +## build-workspace + +Scope: `any`, `root` + +Builds a mirror of the workspace using the packaged production version of each +package. This essentially calls `yarn pack` in each included package and unpacks +the resulting archive in the target `workspace-dir`. + +```text +Usage: backstage-cli build-workspace [options] <workspace-dir> +``` diff --git a/docs/cli/index.md b/docs/cli/index.md new file mode 100644 index 0000000000..1b2e01be62 --- /dev/null +++ b/docs/cli/index.md @@ -0,0 +1,108 @@ +--- +id: index +title: Overview +description: Overview of the Backstage CLI +--- + +## Summary + +Backstage provides an opinionated set of tooling for both frontend and backend +development. It is delivered through the +[`@backstage/cli`](https://www.npmjs.com/package/@backstage/cli) package and +executed either directly through `yarn backstage-cli ` or within +`package.json` scripts. When creating an app using +[`@backstage/create-app`](https://www.npmjs.com/package/@backstage/create-app) +it contains package scripts for executing the most common commands. + +Under the hood the CLI uses [Webpack](https://webpack.js.org/) for bundling, +[Rollup](https://rollupjs.org/) for building packages, +[Jest](https://jestjs.io/) for testing, and [eslint](https://eslint.org/) for +linting. It also includes custom tooling for working within Backstage apps, for +example for keeping the app up to date and verifying static configuration. + +For a full list of CLI commands, see the [commands](./commands.md) page. + +## Introduction + +A goal of Backstage is to provide a delightful developer experience in and +around the project. Creating new apps and plugins should be simple, iteration +speed should be fast, and the overhead of maintaining custom tooling should be +minimal. As a part of accomplishing this goal, Backstage provides its own set of +opinionated tooling, delivered primarily through the +[`@backstage/cli`](https://www.npmjs.com/package/@backstage/cli) package. + +The `@backstage/cli` package provides a single executable script, +`backstage-cli`, which you can run directly with `yarn` or within a script in +`package.json`. If you have a Backstage app set up, you can try out the +following command to print the top-level help page of the CLI: + +```text +yarn backstage-cli --help +``` + +If you are familiar with [`create-react-app`](https://create-react-app.dev/) you +may recognize the pattern of bundling tooling up as a CLI, as it uses a package +called [`react-scripts`](https://www.npmjs.com/package/react-scripts) to bring +most of the functionality into the created project. The Backstage equivalent of +`create-react-app` is +[`@backstage/create-app`](https://www.npmjs.com/package/@backstage/create-app), +and the equivalent of `react-scripts` is `@backstage/cli`. There are however a +couple of key differences between the two. Most notably, Backstage apps are +monorepos and the CLI is tailored for that environment. It provides tooling both +for bundling and developing full end-user apps, but also for developing, +building and publishing individual packages within the monorepo, as well as +tooling that is more unique to Backstage, such as commands for working with +static configuration. + +## Opinionated Tooling + +The Backstage CLI is highly opinionated in what tools are used and how they are +configured. It is tailored for development in large TypeScript monorepos with +hundreds of separate packages, but with the ability to have edits anywhere in +the codebase reflected within a few seconds. The build output is also optimized +for this setup, and aims to provide an excellent user experience with fast page +load times in modern browsers, rather than a wide range of support. + +While the Backstage tooling is opinionated in how to develop and build packages, +it is also possible to use your own tooling either partially or fully. For +example, the CLI provides a command for building a plugin package for +publishing, but the output is a quite standard combination of transpiled +JavaScript and TypeScript type declarations. The usage of the command from the +CLI can therefore easily be replaced with other tools if necessary. + +Just like `react-scripts`, the Backstage CLI does not provide many hooks for +overriding or customizing the build process. This is to allow for evolution of +the CLI without having to take a wide API surface into account. This allows us +to quickly iterate and improve the tooling, as well as to more easily keep +dependencies up to date. + +## Opinions & Goals + +In no particular order, this is a list of opinions and goals that guide the +design and development of the Backstage CLI: + +- All you need for development is `yarn start`, there should be no need to + manually build packages or run other separate tasks. +- Development experience comes first. The toolchain is optimized for keeping + development smooth, rather than making it easy to for example build and + publish packages. +- Type checking and linting is left for text editors and Continuous Integration. + Most text editors provide tooling for these checks, and running them a second + time during compilation slows down iteration speed and consumes more system + resources. +- Backstage is run in modern browsers. We keep transpilation lightweight and + rely on modern technologies such as HTTP/2 to optimize frontend speed. + +## Glossary + +- **Package** - A package in the Node.js ecosystem, often published to a package + registry such as [NPM](https://www.npmjs.com/). +- **Monorepo** - A project layout that consists of multiple packages within a + single project, where packages are able to have local dependencies on each + other. Often enabled through tooling such as [lerna](https://lerna.js.org/) + and [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) +- **Local Package** - One of the packages within a monorepo. These package may + or may not also be published to a package registry. +- **Bundle** - A collection of the deployment artifacts. The output of the + bundling process, which brings a collection of packages into a single + collection of deployment artifacts. diff --git a/docs/conf/defining.md b/docs/conf/defining.md index ead5c4ff67..358498929a 100644 --- a/docs/conf/defining.md +++ b/docs/conf/defining.md @@ -83,6 +83,12 @@ object will be available in the frontend. The full ancestry does not need to have correctly defined visibilities however, so it is enough to only for example declare the visibility of a leaf node of `type: "string"`. +| `visibility` | | +| ------------ | ------------------------------------------------------------------ | +| `frontend` | Visible in frontend and backend | +| `backend` | (Default) Only in backend | +| `secret` | Only in backend and may be excluded from logs for security reasons | + ## Validation Schemas can be validated using the `backstage-cli config:check` command. If you diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 72e6388273..763acd2261 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -444,21 +444,6 @@ Apart from being a string, the software catalog leaves the format of this field open to implementers to choose. Most commonly, it is set to the ID or email of a group of people in an organizational structure. -### `spec.implementsApis` [optional] - -**NOTE**: This field was marked for deprecation on Nov 25nd, 2020. It will be -removed entirely from the model on Dec 14th, 2020 in the repository and will not -be present in released packages following the next release after that. Please -update your code to not consume this field before the removal date. - -Links APIs that are implemented by the component, e.g. `artist-api`. This field -is optional. - -The software catalog expects a list of one or more strings that references the -names of other entities of the `kind` `API`. - -This field has the same behavior as `spec.providesApis`. - ### `spec.providesApis` [optional] Links APIs that are provided by the component, e.g. `artist-api`. This field is @@ -723,6 +708,10 @@ metadata: description: The infra business unit spec: type: business-unit + profile: + displayName: Infrastructure + email: infrastructure@example.com + picture: https://example.com/groups/bu-infrastructure.jpeg parent: ops children: [backstage, other] ``` @@ -747,6 +736,14 @@ Some common values for this field could be: - `product-area` - `root` - as a common virtual root of the hierarchy, if desired +### `spec.profile` [optional] + +Optional profile information about the group, mainly for display purposes. All +fields of this structure are also optional. The email would be a group email of +some form, that the group may wish to be used for contacting them. The picture +is expected to be a URL pointing to an image that's representative of the group, +and that a browser could fetch and render on a group page or similar. + ### `spec.parent` [optional] The immediate parent group in the hierarchy, if any. Not all groups must have a diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index 16131a1ded..50cadc7b27 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -96,26 +96,38 @@ Build a widget (plugin) framework so that contributors can easily contribute features to TechDocs - that others can use. And, also, so that we can easily migrate Spotify's existing TechDocs features to open source. -## Structure +## Platforms Supported -- [Getting Started] -- [Concepts] -- [Creating and Publishing Documentation] -- [FAQ] +See [TechDocs Architecture](architecture.md) to get an overview of where these +providers are used. + +| Source Code Hosting Provider | Support Status | +| ---------------------------- | -------------- | +| GitHub | Yes ✅ | +| GitHub Enterprise | Yes ✅ | +| BitBucket | Yes ✅ | +| Azure DevOps | Yes ✅ | +| GitLab | Yes ✅ | +| GitLab Enterprise | Yes ✅ | + +| File Storage Provider | Support Status | Track status | +| --------------------------------- | -------------- | ----------------------------------------------------------- | +| Local Filesystem of Backstage app | Yes ✅ | | +| Google Cloud Storage (GCS) | Yes ✅ | | +| Amazon Web Services (AWS) S3 | No ❌ | [#3714](https://github.com/backstage/backstage/issues/3714) | +| Azure Storage | No ❌ | | + +[Reach out to us](#feedback) if you want to request more platforms. ## Tech Stack -| Stack | Location | -| ------------------------------------------- | -------------------------------------------------------- | -| Frontend | [`@backstage/plugin-techdocs`][techdocs/frontend] | -| Backend | [`@backstage/plugin-techdocs-backend`][techdocs/backend] | -| Docker Container (for generating doc sites) | [`techdocs-container`][techdocs/container] | -| CLI (for local development) | [`@techdocs/cli`][techdocs/cli] | +| Stack | Location | +| ----------------------------------------------- | -------------------------------------------------------- | +| Frontend Plugin | [`@backstage/plugin-techdocs`][techdocs/frontend] | +| Backend Plugin | [`@backstage/plugin-techdocs-backend`][techdocs/backend] | +| CLI (for local development and generating docs) | [`@techdocs/cli`][techdocs/cli] | +| Docker Container (for generating docs) | [`techdocs-container`][techdocs/container] | -[getting started]: getting-started.md -[concepts]: concepts.md -[creating and publishing documentation]: creating-and-publishing.md -[faq]: FAQ.md 'Frequently asked questions' [techdocs/frontend]: https://github.com/backstage/backstage/blob/master/plugins/techdocs [techdocs/backend]: @@ -131,3 +143,6 @@ https://docs.google.com/forms/d/e/1FAIpQLSdn5Vn3MQhCdyYRuW8cMzZkMQF0bFxXYN168gZR This is to gather inputs from you (the Backstage community) which will help us best serve TechDocs adopters and existing users. Your inputs will shape our roadmap and we will share it in the open. + +For any other general queries, reach out to us in the `#docs-like-code` channel +of our [Discord chatroom](https://github.com/backstage/backstage#community). diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 9305239b7f..3969002a46 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -50,8 +50,8 @@ built. We assume each entity lives in a repository somewhere (GitHub, GitLab, etc.). We recommend using a CI/CD pipeline with the repository that has a dedicated -step/job to build docs for TechDocs. The generated static files are then stored -in a cloud storage solution of your choice. +step/job to generate docs for TechDocs. The generated static files are then +stored in a cloud storage solution of your choice. [Track progress here](https://github.com/backstage/backstage/issues/3096). Similar to how it is done in the Basic setup, the TechDocs Reader requests @@ -60,10 +60,10 @@ your configured storage solution for the necessary files and returns them to TechDocs Reader. We will provide instructions, scripts and/or templates (e.g. GitHub Actions) to -build docs in your CI/CD system. +generate docs in your CI/CD system. [Track progress here.](https://github.com/backstage/backstage/issues/3400) You -will be able to use `techdocs-cli` to build docs and publish the generated docs -site files to your cloud storage system. +will be able to use `techdocs-cli` to generate docs and publish the generated +docs site files to your cloud storage system. Note about caching: We have noticed internally that some storage providers can be quite slow, which is why we are recommending a cache that sits between the @@ -120,8 +120,8 @@ docs site in real-time?** A: Generating the content from Markdown on the fly is not optimal (although that is how the basic out-of-the-box setup is implemented). Storage solutions act as a cache for the generated static content. TechDocs is also currently built on -MkDocs which does not allow us to build docs per-page, so we would have to build -all docs for a entity on every request. +MkDocs which does not allow us to generate docs per-page, so we would have to +build all docs for a entity on every request. # Future work diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 1675c64288..26254a7e12 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -8,15 +8,45 @@ Spotify's docs-like-code solution in Backstage This page describes concepts that are introduced with Spotify's docs-like-code solution in Backstage. -### TechDocs Core Plugin +## Generating TechDocs Steps -The TechDocs Core Plugin is an [MkDocs](https://www.mkdocs.org/) plugin created -as a wrapper around multiple MkDocs plugins and Python Markdown extensions to -standardize the configuration of MkDocs used for TechDocs. +### TechDocs Preparer -[TechDocs Core](https://github.com/backstage/mkdocs-techdocs-core) +Preparing is the first step of generating documentation for an entity. It +fetches the source markdown files from the source code hosting provider (GitHub, +GitLab, etc.) and passes the files to the generator for next steps. -### TechDocs container +There are two kinds of preparers available - + +1. Common Git Preparer - Uses `git clone` on any repository url. +2. Url Reader - Uses source code hosting provider's API to download files. + (Faster and recommended) + +### TechDocs Generator + +Generating is the second step after preparing the markdown source files. This +step either runs the TechDocs container (defined below) or runs `mkdocs` CLI to +generate static HTML files and its assets. + +### TechDocs Publisher + +Publishing is the third and final step after preparing and generating docs. +TechDocs Publisher uploads the generated files to a storage. + +The `techdocs-backend` plugin currently comes with two publishers - Google Cloud +Storage and Local Filesystem. You can configure them in your Backstage app. +[See here](./configuration.md). + +A TechDocs publisher is responsible for two things (two-way communication +between `techdocs-backend` and the storage) + +1. Publish generated static files to a storage (Configured by + `techdocs.builder`) +2. Read files from the storage when users visit a TechDocs site + +[TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) + +## TechDocs Container The TechDocs container is a Docker container available at [DockerHub](https://hub.docker.com/r/spotify/techdocs). It builds static HTML @@ -25,16 +55,15 @@ MkDocs. [TechDocs Container](https://github.com/backstage/techdocs-container) -### TechDocs publisher +## TechDocs Core Plugin -The `techdocs-backend` plugin currently comes with one publisher - -`LocalPublish`. +The TechDocs Core Plugin is an [MkDocs](https://www.mkdocs.org/) plugin created +as a wrapper around multiple MkDocs plugins and Python Markdown extensions to +standardize the configuration of MkDocs used for TechDocs. -[TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) +[TechDocs Core](https://github.com/backstage/mkdocs-techdocs-core) -More standalone publishers will come in the near future... - -### TechDocs CLI +## TechDocs CLI The TechDocs CLI was created to make it easy to write, generate and preview documentation for publishing. Currently it mostly acts as a wrapper around the @@ -43,7 +72,7 @@ container. [TechDocs CLI](https://github.com/backstage/techdocs-cli) -### TechDocs Reader +## TechDocs Reader Documentation generated by TechDocs is generated as static HTML sites. The TechDocs Reader was therefore created to be able to integrate pre-generated HTML @@ -55,7 +84,7 @@ TechDocs widgets for a customized full-featured TechDocs experience. [TechDocs Reader](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/reader/README.md) -### Transformers +## Transformers Transformers are different pieces of functionality used inside the TechDocs Reader. The reason why transformers were introduced was to provide a way to diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md new file mode 100644 index 0000000000..b3cb349778 --- /dev/null +++ b/docs/features/techdocs/configuration.md @@ -0,0 +1,73 @@ +--- +id: configuration +title: TechDocs Configuration Options +description: + Reference documentation for configuring TechDocs using app-config.yaml +--- + +Using the `app-config.yaml` in the Backstage app, you can configure TechDocs +using several options. This page serves as a reference to all the available +configuration options for TechDocs. + +```yaml +# File: app-config.yaml + +techdocs: + + # TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc. + + requestUrl: http://localhost:7000/api/techdocs + + + # Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware + # to serve files from either a local directory or an External storage provider. + + storageUrl: http://localhost:7000/api/techdocs/static/docs + + + # generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to + # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). + # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running + # into Docker in Docker situation. Read more here + # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + + generators: + techdocs: 'docker' + + + # techdocs.builder can be either 'local' or 'external. + # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage + # and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture. + # If builder is set to 'external', techdocs-backend will only fetch the docs and will NOT try to generate and publish. In this case of 'external', + # we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). This is the "Recommended" setup of + # the architecture. Read more here https://backstage.io/docs/features/techdocs/architecture + + builder: 'local' + + + # techdocs.publisher is used to configure the Storage option, whether you want to use the local filesystem to store generated docs + # or you want to use External storage providers like Google Cloud Storage, AWS S3, etc. + + publisher: + + # techdocs.publisher.type can be - 'local' or 'googleGcs' (awsS3, azureStorage, etc. to be available as well). + # When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files. + # When set to 'googleGcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files. + + type: 'local' + + + # Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise. + + googleGcs: + # An API key is required to write to a storage bucket. + credentials: + $file: '/path/to/google_application_credentials.json', + + # Your GCP Project ID where the Cloud Storage Bucket is hosted. + projectId: 'gcp-project-id' + + # Cloud Storage Bucket Name + bucketName: 'techdocs-storage', + +``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index e51c6a122e..c82bc8b95b 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -1,7 +1,7 @@ --- id: getting-started title: Getting Started -description: Getting Started Guidelines +description: Getting Started Documentation --- TechDocs functions as a plugin to Backstage, so you will need to use Backstage @@ -10,50 +10,156 @@ to use TechDocs. If you haven't setup Backstage already, start [here](../../getting-started/index.md). -## Installing TechDocs +> If you used `npx @backstage/create-app`, TechDocs may already be present. +> +> You should skip to [`Setting the Configuration`](#setting-the-configuration) +> below. -TechDocs is provided with the Backstage application by default. If you want to -set up TechDocs manually, keep following the instructions below. - -### Adding the package +## Adding TechDocs frontend plugin The first step is to add the TechDocs plugin to your Backstage application. -Navigate to your new Backstage application folder: - -```bash -cd hello-world/ -``` - -Then navigate to your `packages/app` folder to install TechDocs: +Navigate to your new Backstage application directory. And then to your +`packages/app` directory, and install the `@backstage/plugin-techdocs` package. ```bash +cd my-backstage-app/ cd packages/app yarn add @backstage/plugin-techdocs ``` -After a short while, the TechDocs plugin should be successfully installed. +Once the package has been installed, you need to import the plugin in your app. -Next, you need to set up some basic configuration. Enter the following command: - -```bash -yarn install -``` - -Add this to `packages/app/src/plugins.ts`: +Add the following to `packages/app/src/plugins.ts`: ```typescript export { plugin as TechDocs } from '@backstage/plugin-techdocs'; ``` -### Setting the configuration +Now let us embed the TechDocs router in our main Backstage frontend router. In +`packages/app/src/App.tsx`, import the TechDocs router and add the following to +`AppRoutes`: -TechDocs allows for configuration of the docs storage URL through your -`app-config.yaml` file. We provide two different values to be configured, -`requestUrl` and `storageUrl`. The `requestUrl` is what the reader will request -its data from, and `storageUrl` is where the backend can find the stored -documentation. +```tsx +import { Router as DocsRouter } from '@backstage/plugin-techdocs'; -The default storage and request URLs: +// ... + +const AppRoutes = () => { + + // ... other plugin routes + } /> + ; +}; +``` + +That's it! But now, we need the TechDocs Backend plugin for the frontend to +work. + +## Adding TechDocs Backend plugin + +Navigate to `packages/backend` of your Backstage app, and install the +`@backstage/plugin-techdocs-backend` package. + +```bash +cd my-backstage-app/ +cd packages/backend +yarn add @backstage/plugin-techdocs-backend +``` + +Create a file called `techdocs.ts` inside `packages/backend/src/plugins/` and +add the following + +```typescript +import { + createRouter, + DirectoryPreparer, + Preparers, + Generators, + TechdocsGenerator, + CommonGitPreparer, + UrlPreparer, + Publisher, +} from '@backstage/plugin-techdocs-backend'; +import { PluginEnvironment } from '../types'; +import Docker from 'dockerode'; + +export default async function createPlugin({ + logger, + config, + discovery, + reader, +}: PluginEnvironment) { + // Preparers are responsible for fetching source files for documentation. + const preparers = new Preparers(); + + const directoryPreparer = new DirectoryPreparer(logger); + preparers.register('dir', directoryPreparer); + + const commonGitPreparer = new CommonGitPreparer(logger); + preparers.register('github', commonGitPreparer); + preparers.register('gitlab', commonGitPreparer); + preparers.register('azure/api', commonGitPreparer); + + const urlPreparer = new UrlPreparer(reader, logger); + preparers.register('url', urlPreparer); + + // Generators are used for generating documentation sites. + const generators = new Generators(); + const techdocsGenerator = new TechdocsGenerator(logger, config); + generators.register('techdocs', techdocsGenerator); + + // Publishers are used for + // 1. Publishing generated files to storage + // 2. Fetching files from storage and passing them to TechDocs frontend. + const publisher = Publisher.fromConfig(config, logger, discovery); + + // Docker client used by the generators. + const dockerClient = new Docker(); + + return await createRouter({ + preparers, + generators, + publisher, + dockerClient, + logger, + config, + discovery, + }); +} +``` + +You may need to install the `dockerode` package. But you may already have it in +your backend since [Scaffolder plugin](../software-templates/index.md) also uses +it. + +See [Concepts](concepts.md) and [TechDocs Architecture](architecture.md) to +learn more about how preparers, generators and publishers work. + +Final step is to import the techdocs backend plugin in Backstage app backend. +Add the following to your `packages/backend/src/index.ts`: + +```typescript +import techdocs from './plugins/techdocs'; + +// .... main should already be present. +async function main() { + // ... other backend plugin envs + const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + + // ... other backend plugin routes + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); +} +``` + +That's it! TechDocs frontend and backend have now been added to your Backstage +app. Now let us tweak some configurations to suit your needs. + +## Setting the configuration + +**See [TechDocs Configuration Options](configuration.md) for complete +configuration reference.** + +### Setting TechDocs URLs ```yaml techdocs: @@ -61,22 +167,59 @@ techdocs: requestUrl: http://localhost:7000/api/techdocs/ ``` -If you want `techdocs-backend` to manage building and publishing, you want -`requestUrl` to point to the default value (or wherever `techdocs-backend` is -hosted). `storageUrl` should be where your publisher publishes your docs. Using -the default `LocalPublish` that is the default value. +`requestUrl` is used by TechDocs frontend plugin to discover techdocs-backend +endpoints, and the `storageUrl` is another endpoint in `techdocs-backend` which +acts as a middleware between TechDocs and the storage (where the static +generated docs site are stored). These default values should mostly work for +you. These options will soon be optional to set. -If you have a setup where you are not using `techdocs-backend` for managing -building and publishing of your documentation, you want to change the -`requestUrl` to point to your storage. In this case `storageUrl` is not -required. +### Should TechDocs Backend generate docs? -### Disable Docker in Docker situation (Optional) +```yaml +techdocs: + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/ + builder: 'local' +``` -The TechDocs backend plugin runs a docker container with mkdocs installed to +Set `techdocs.builder` to `'local'` if you want your TechDocs Backend to be +responsible for generating documentation sites. If set to `'external'`, +Backstage will assume that the sites are being generated on each entity's CI/CD +pipeline, and are being stored in a storage somewhere. + +When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a +read-only experience where it serves static files from a storage containing all +the generated documentation. Read more in the "Basic" and "Recommended" setup of +TechDocs [here](architecture.md) + +### Choosing storage (publisher) + +TechDocs needs to know where to store generated documentation sites and where to +fetch the sites from. This is managed by a +[Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage, +Amazon S3, or local filesystem of Backstage server. + +It is okay to use the local filesystem in a "Basic" setup when you are trying +out Backstage for the first time. Using Cloud Storage is documented +[here](./using-cloud-storage.md). + +```yaml +techdocs: + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/ + builder: 'local' + publisher: + type: 'local' +``` + +### Disabling Docker in Docker situation (Optional) + +You can skip this if your `techdocs.builder` is set to `'external'`. + +The TechDocs Backend plugin runs a docker container with mkdocs installed to generate the frontend of the docs from source files (Markdown). If you are deploying Backstage using Docker, this will mean that your Backstage Docker -container will try to run another Docker container for TechDocs backend. +container will try to run another Docker container for TechDocs Backend. To avoid this problem, we have a configuration available. You can set a value in your `app-config.yaml` that tells the techdocs generator if it should run the @@ -108,23 +251,13 @@ the dependencies. For example, we want `Markdown` version to be You can also explicitly install `Markdown==3.2.2` after installing all other Python packages. -## Run Backstage locally +## Running Backstage locally -Change folder to `/packages/backend` and run the -following command: +Start the frontend and the backend app by +[running backstage locally](../../getting-started/running-backstage-locally.md). -```bash -yarn start -``` - -Open a new command line window. Change directory to your Backstage application -root and run the following command: - -```bash -yarn start -``` - -Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/). +Open your browser at [http://localhost:3000/docs/](http://localhost:3000/docs/) +to see all your documentation sites. ## Additional reading diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md new file mode 100644 index 0000000000..c7b5ec9a9f --- /dev/null +++ b/docs/features/techdocs/using-cloud-storage.md @@ -0,0 +1,95 @@ +--- +id: using-cloud-storage +title: Using Cloud Storage for TechDocs generated files +description: Using Cloud Storage for TechDocs generated files +--- + +In the [TechDocs architecture](./architecture.md) you have the option to choose +where you want to store the Generated static files which TechDocs uses to render +documentation. In both the "Basic" and "Recommended" setup, you can add cloud +storage providers like Google GCS, Amazon AWS S3, etc. By default, TechDocs uses +the local filesystem of the `techdocs-backend` plugin in the "Basic" setup. And +in the recommended setup, having one of the cloud storage is a prerequisite. +Read more on the TechDocs Architecture documentation page. + +On this page you can read how to enable them. + +## Configuring Google GCS Bucket with TechDocs + +Follow the +[official Google Cloud documentation](https://googleapis.dev/nodejs/storage/latest/index.html#quickstart) +for the latest instructions on the following steps involving GCP. + +**1. Set `techdocs.publisher.type` config in your `app-config.yaml`** + +Set `techdocs.publisher.type` to `'googleGcs'`. + +```yaml +techdocs: + publisher: + type: 'googleGcs' +``` + +**2. GCP (Google Cloud Platform) Project** + +Create or choose a dedicated GCP project. Set +`techdocs.publisher.googleGcs.projectId` to the project ID. + +```yaml +techdocs: + publisher: + type: 'googleGcs' + googleGcs: + projectId: 'gcp-project-id' +``` + +**3. Service account API key** + +Create a new Service Account and a key associated with it. In roles of the +service account, use "Storage Admin". + +If you want to create a custom role, make sure to include both `get` and +`create` permissions for both "Objects" and "Buckets". See +https://cloud.google.com/storage/docs/access-control/iam-permissions + +A service account can have many keys. Open your newly created account's page (in +IAM & Admin console), and create a new key. Use JSON format for the key. + +A `.json` file will be downloaded. This is the secret +key TechDocs will use to make API calls. Make it available in your Backstage +server and/or your local development server and set it in the app config +`techdocs.publisher.googleGcs.credentials`. + +```yaml +techdocs: + publisher: + type: 'googleGcs' + googleGcs: + projectId: 'gcp-project-id' + credentials: + $file: '/path/to/google_application_credentials.json' +``` + +**4. GCS Bucket** + +Create a dedicated bucket for TechDocs sites. techdocs-backend will publish +documentation to this bucket. TechDocs will fetch files from here to serve +documentation in Backstage. + +Set the name of the bucket to `techdocs.publisher.googleGcs.bucketName`. + +```yaml +techdocs: + publisher: + type: 'googleGcs' + googleGcs: + projectId: 'gcp-project-id' + credentials: + $file: '/path/to/google_application_credentials.json' + bucketName: 'name-of-techdocs-storage-bucket' +``` + +**5. That's it!** + +Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to +store the static generated documentation files. diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 71612cc5e4..a75c000b8a 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -47,55 +47,40 @@ guidelines to get started. ### Ongoing work 🚧 -- **[Plugins for managing micro services end-2-end](https://github.com/backstage/backstage/milestone/14)** - Out of the box Backstage will ship with a set of plugins (Overview, CI, API - and Docs) that will demonstrate how a user can manage a micro service and - follow a change all the way out in production. Completing this work will make - it much easier to see how a plugin can be built that integrates with the - Backstage Service Catalog. - -- **[Users and teams](https://github.com/backstage/backstage/issues/1807)** - Ownership is a central concept in Backstage. It should be easy to import your - existing organizational data, such as users and groups/teams, into Backstage. - A user the logs into Backstage should see software components owned by the - team(s) they are in. - - **[Backstage platform is stable](https://github.com/backstage/backstage/milestone/19)** - - The platform APIs and features are stable and can be depended on for + The core of Backstage and its features are stable and can be depended on for production use. After this plugins will require little to no maintenance. -* **[Improved Kubernetes plugin](https://github.com/backstage/backstage/issues/2857)** - +- **[Improved Kubernetes plugin](https://github.com/backstage/backstage/issues/2857)** - Native support for Kubernetes, making it easier for developers to see and manage their services running in k8s. -- Further improvements to platform documentation. Examples include a Golden Path - for plugin development. +- **Global search** - Extend the basic search available in the Backstage Service + Catalog with a global search experience. + +- **[Software Templates V2](https://github.com/backstage/backstage/issues/2771)** - + Expanding the templates to make steps more composable, by adding the ability + to add more steps for custom logic like webhooks and using authorization from + integrations. ### Future work 🔮 -- **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage - deployment available publicly so that people can click around and get a feel - for the product without having to install anything. +- **Golden Path for Plugin Development** - Creating an easy, standardized way + for developers to build plugins will encourage contributions and lead to a + richer ecosystem for everyone. -- **[Global search](https://github.com/backstage/backstage/issues/1499)** - - Extend the basic search available in the Backstage Service Catalog with a - global search experience. Long term this search solution should be extensible, - making it possible for you add custom search results. - -- **[[TechDocs V.2] Stabilization release](https://github.com/backstage/backstage/milestone/17)** - - Platform stability and compatibility improvements. - -- **Additional auth providers** - Backstage should work for most (all!) auth - solutions. Since Backstage can be used by companies regardless of what cloud - (or on prem) you are using we are especially keen to get auth support for - [AWS](https://github.com/backstage/backstage/issues/290), - [Azure](https://github.com/backstage/backstage/issues/348) and others. - -- **[Initial GraphQL API](https://github.com/backstage/backstage/milestone/13)** - - A GraphQL API will open up the rich metadata provided by Backstage in a single +- **[GraphQL API](https://github.com/backstage/backstage/milestone/13)** - A + GraphQL API will open up the rich metadata provided by Backstage in a single query. Plugins can easily query this API as well as extend the model where needed. +- **Inter Plugin Communication** - **[Under consideration]** Establish more + clearly defined patterns for plugins to communicate. + +- **Improved Access Control** - **[Under consideration]** Provide finer grained + access controls and management making it possible for you to better control + the platforms user experience. + ### Plugins Building and maintaining [plugins](https://backstage.io/plugins) is the work of @@ -110,8 +95,17 @@ Are you missing a plugin for your favorite tool? Please [suggest a new one](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). Chances are that someone will jump in and help build it. +### Community Initiatives 🧑‍🤝‍🧑 (Coming soon) + +- **Backstage Monthly Meetup** - A space for the community to come together to + share and learn about the latest happenings in Backstage. + +- **Backstage Hackathons** - Open to everyone in our Backstage community, a + celebration of you, the project and building awesome things together + ### Completed milestones ✅ +- [Deploy a product demo at `demo.backstage.io`](https://demo.backstage.io) - [Kubernetes plugin - v1](https://github.com/backstage/backstage/tree/master/plugins/kubernetes) - [Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage) - [Backstage Design System 💅](https://backstage.io/blog/2020/09/30/backstage-design-system) diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md index 567e9dcbff..c53193f063 100644 --- a/docs/overview/stability-index.md +++ b/docs/overview/stability-index.md @@ -39,28 +39,28 @@ TL;DR: ## Packages -### [`example-app`](https://github.com/backstage/backstage/tree/master/packages/app/) +### `example-app` [GitHub](https://github.com/backstage/backstage/tree/master/packages/app/) This is the `packages/app` package, and it serves as an example as well as utility for local development in the main Backstage repo. Stability: `N/A` -### [`example-backend`](https://github.com/backstage/backstage/tree/master/packages/backend/) +### `example-backend` [GitHub](https://github.com/backstage/backstage/tree/master/packages/backend/) This is the `packages/backend` package, and it serves as an example as well as utility for local development in the main Backstage repo. Stability: `N/A` -### [`backend-common`](https://github.com/backstage/backstage/tree/master/packages/backend-common/) +### `backend-common` [GitHub](https://github.com/backstage/backstage/tree/master/packages/backend-common/) A collection of common helpers to be used by both backend plugins, and for constructing backend packages. Stability: `1` -### [`catalog-client`](https://github.com/backstage/backstage/tree/master/packages/catalog-client/) +### `catalog-client` [GitHub](https://github.com/backstage/backstage/tree/master/packages/catalog-client/) An HTTP client for interacting with the catalog backend. Usable both in frontend and Backend. @@ -68,7 +68,7 @@ and Backend. Stability: `0`. This is a very new addition and we have some immediate changes planned. -### [`catalog-model`](https://github.com/backstage/backstage/tree/master/packages/catalog-model/) +### `catalog-model` [GitHub](https://github.com/backstage/backstage/tree/master/packages/catalog-model/) Contains the core catalog model, and utilities for working with entities. Usable both in frontend and Backend. @@ -77,7 +77,7 @@ Stability: `2`. The catalog model is evolving, but because of the broad usage we want to ensure some stability. -### [`cli`](https://github.com/backstage/backstage/tree/master/packages/cli/) +### `cli` [GitHub](https://github.com/backstage/backstage/tree/master/packages/cli/) The main toolchain used for Backstage development. The various CLI commands and options passed to those commands, as well as the environment variables read by @@ -87,27 +87,27 @@ unless it is likely to affect external tooling. Stability: `2` -### [`cli-common`](https://github.com/backstage/backstage/tree/master/packages/cli-common/) +### `cli-common` [GitHub](https://github.com/backstage/backstage/tree/master/packages/cli-common/) Lightweight utilities used by the various Backstage CLIs, not intended for external use. Stability: `N/A` -### [`config`](https://github.com/backstage/backstage/tree/master/packages/config/) +### `config` [GitHub](https://github.com/backstage/backstage/tree/master/packages/config/) Provides the logic and interfaces for reading static configuration. Stability: `2` -### [`config-loader`](https://github.com/backstage/backstage/tree/master/packages/config-loader/) +### `config-loader` [GitHub](https://github.com/backstage/backstage/tree/master/packages/config-loader/) Used to load in static configuration, mainly for use by the CLI and @backstage/backend-common. Stability: `1`. Mainly intended for internal use. -### [`core`](https://github.com/backstage/backstage/tree/master/packages/core/) +### `core` [GitHub](https://github.com/backstage/backstage/tree/master/packages/core/) #### Section: React Components @@ -147,51 +147,62 @@ ErrorApi, IdentityApi, the auth APIs, etc. Stability: `1`. Most changes to the core utility APIs will not lead to widespread breaking changes since most apps rely on the default implementations. -### [`core-api`](https://github.com/backstage/backstage/tree/master/packages/core-api/) +### `core-api` [GitHub](https://github.com/backstage/backstage/tree/master/packages/core-api/) The non-visual parts of @backstage/core. Everything in this packages is re-exported from @backstage/core, and this package should not be used directly. Stability: See @backstage/core -### [`create-app`](https://github.com/backstage/backstage/tree/master/packages/create-app/) +### `create-app` [GitHub](https://github.com/backstage/backstage/tree/master/packages/create-app/) The CLI used to scaffold new Backstage projects. Stability: `2` -### [`dev-utils`](https://github.com/backstage/backstage/tree/master/packages/dev-utils/) +### `dev-utils` [GitHub](https://github.com/backstage/backstage/tree/master/packages/dev-utils/) Provides utilities for developing plugins in isolation. Stability: `0`. This package is largely broken and needs updates. -### [`docgen`](https://github.com/backstage/backstage/tree/master/packages/docgen/) +### `docgen` [GitHub](https://github.com/backstage/backstage/tree/master/packages/docgen/) Internal CLI utility for generating API Documentation. Stability: `N/A` -### [`e2e-test`](https://github.com/backstage/backstage/tree/master/packages/e2e-test/) +### `e2e-test` [GitHub](https://github.com/backstage/backstage/tree/master/packages/e2e-test/) Internal CLI utility for running e2e tests. Stability: `N/A` -### [`storybook`](https://github.com/backstage/backstage/tree/master/packages/storybook/) +### `integration` [GitHub](https://github.com/backstage/backstage/tree/master/packages/integration/) + +Provides shared utilities for managing integrations towards different types of +third party systems. This package is currently internal and its functionality +will likely be exposed via separate APIs in the future. + +Some of the functionality in this package is not available elsewhere yes, so if +it's necessary it can be used, but there will be breaking changes. + +Stability: `0` + +### `storybook` [GitHub](https://github.com/backstage/backstage/tree/master/packages/storybook/) Internal storybook build for publishing stories to https://backstage.io/storybook Stability: `N/A` -### [`test-utils`](https://github.com/backstage/backstage/tree/master/packages/test-utils/) +### `test-utils` [GitHub](https://github.com/backstage/backstage/tree/master/packages/test-utils/) Utilities for writing tests for Backstage plugins and apps. Stability: `2` -### [`test-utils-core`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core/) +### `test-utils-core` [GitHub](https://github.com/backstage/backstage/tree/master/packages/test-utils-core/) Internal testing utilities that are separated out for usage in @backstage/core-api. All exports are re-exported by @backstage/test-utils. This @@ -199,7 +210,7 @@ package should not be depended on directly. Stability: See @backstage/test-utils -### [`theme`](https://github.com/backstage/backstage/tree/master/packages/theme/) +### `theme` [GitHub](https://github.com/backstage/backstage/tree/master/packages/theme/) The core Backstage MUI theme along with customization utilities. @@ -229,21 +240,21 @@ Any plugin that is not listed below is untracked and can generally be considered unstable with a score of `0`. Open a Pull Request if you want your plugin to be added! -### [`api-docs`](https://github.com/backstage/backstage/tree/master/plugins/api-docs/) +### `api-docs` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/api-docs/) Components to discover and display API entities as an extension to the catalog plugin. Stability: `0` -### [`app-backend`](https://github.com/backstage/backstage/tree/master/plugins/app-backend/) +### `app-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/app-backend/) A backend plugin that can be used to serve the frontend app and inject configuration. Stability: `2` -### [`auth-backend`](https://github.com/backstage/backstage/tree/master/plugins/auth-backend/) +### `auth-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/auth-backend/) A backend plugin that implements the backend portion of the various authentication flows used in Backstage. @@ -256,7 +267,7 @@ Stability: `2` Stability: `1` -### [`catalog`](https://github.com/backstage/backstage/tree/master/plugins/catalog/) +### `catalog` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/catalog/) The frontend plugin for the catalog, with the table and building blocks for the entity pages. @@ -264,7 +275,7 @@ entity pages. Stability: `1`. We're planning some work to overhaul how entity pages are constructed. -### [`catalog-backend`](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend/) +### `catalog-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend/) The backend API for the catalog, also exposes the processing subsystem for customization of the catalog. Powers the @backstage/plugin-catalog frontend @@ -278,53 +289,53 @@ Stability: `1`. There are plans to remove and rework some endpoints. Stability: `1`. There are plans to rework parts of the Processor interface. -### [`catalog-graphql`](https://github.com/backstage/backstage/tree/master/plugins/catalog-graphql/) +### `catalog-graphql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/catalog-graphql/) Provides the catalog schema and resolvers for the graphql backend. Stability: `0`. Under heavy development and subject to change. -### [`explore`](https://github.com/backstage/backstage/tree/master/plugins/explore/) +### `explore` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/explore/) A frontend plugin that introduces the concept of exploring internal and external tooling in an organization. Stability: `0`. Only an example at the moment and not customizable. -### [`graphiql`](https://github.com/backstage/backstage/tree/master/plugins/graphiql/) +### `graphiql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/graphiql/) Integrates GraphiQL as a tool to browse GraphQL API endpoints inside Backstage. Stability: `1` -### [`graphql`](https://github.com/backstage/backstage/tree/master/plugins/graphql/) +### `graphql` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/graphql/) A backend plugin that provides Stability: `0`. Under heavy development and subject to change. -### [`kubernetes`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes/) +### `kubernetes` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/kubernetes/) The frontend component of the Kubernetes plugin, used to browse and visualize Kubernetes resources. Stability: `1`. -### [`kubernetes-backend`](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend/) +### `kubernetes-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/kubernetes-backend/) The backend component of the Kubernetes plugin, used to fetch Kubernetes resources from clusters and associate them with entities in the Catalog. Stability: `1`. -### [`proxy-backend`](https://github.com/backstage/backstage/tree/master/plugins/proxy-backend/) +### `proxy-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/proxy-backend/) A backend plugin used to set up proxying to other endpoints based on static configuration. Stability: `1` -### [`register-component`](https://github.com/backstage/backstage/tree/master/plugins/register-component/) +### `register-component` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/register-component/) A frontend plugin that allows the user to register entity locations in the catalog. @@ -332,14 +343,14 @@ catalog. Stability: `0`. This plugin is likely to be replaced by a generic entity import plugin instead. -### [`scaffolder`](https://github.com/backstage/backstage/tree/master/plugins/scaffolder/) +### `scaffolder` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/scaffolder/) The frontend scaffolder plugin where one can browse templates and initiate scaffolding jobs. Stability: `1` -### [`scaffolder-backend`](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/) +### `scaffolder-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/) The backend scaffolder plugin that provides an implementation for templates in the catalog. @@ -347,35 +358,35 @@ the catalog. Stability: `1`. There is planned work to rework the scaffolder in https://github.com/backstage/backstage/issues/2771. -### [`tech-radar`](https://github.com/backstage/backstage/tree/master/plugins/tech-radar/) +### `tech-radar` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/tech-radar/) Visualize the your company's official guidelines of different areas of software development. Stability: `0` -### [`techdocs`](https://github.com/backstage/backstage/tree/master/plugins/techdocs/) +### `techdocs` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/techdocs/) The frontend component of the TechDocs plugin, used to browse technical documentation of entities. Stability: `1` -### [`techdocs-backend`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/) +### `techdocs-backend` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/) The backend component of the TechDocs plugin, used to transform and serve TechDocs. Stability: `0` -### [`user-settings`](https://github.com/backstage/backstage/tree/master/plugins/user-settings/) +### `user-settings` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/user-settings/) A frontend plugin that provides a page where the user can tweak various settings. Stability: `1` -### [`welcome`](https://github.com/backstage/backstage/tree/master/plugins/welcome/) +### `welcome` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/welcome/) A plugin that can be used to welcome the user to Backstage. diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index bcea80e26b..622c085291 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -27,7 +27,7 @@ To inspect the state of a feature flag inside your plugin, you can use the `FeatureFlagsApi`, accessed via the `featureFlagsApiRef`. For example: ```tsx -import React, { FC } from 'react'; +import React from 'react'; import { Button } from '@material-ui/core'; import { featureFlagsApiRef, useApi } from '@backstage/core'; diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md index 216ff9659f..0f8d9bb84b 100644 --- a/docs/tutorials/quickstart-app-plugin.md +++ b/docs/tutorials/quickstart-app-plugin.md @@ -81,13 +81,13 @@ import { useApi } from '@backstage/core-api'; _from inline:_ ```tsx -const ExampleComponent: FC<{}> = () => ( ... ) +const ExampleComponent = () => ( ... ) ``` _to block:_ ```tsx -const ExampleComponent: FC<{}> = () => { +const ExampleComponent = () => { return ( ... @@ -135,7 +135,7 @@ changes, let's start by wiping this component clean. 1. Replace everything in the file with the following: ```tsx -import React, { FC } from 'react'; +import React from 'react'; import { useAsync } from 'react-use'; import Alert from '@material-ui/lab/Alert'; import { @@ -147,7 +147,7 @@ import { import { useApi } from '@backstage/core-api'; import { graphql } from '@octokit/graphql'; -const ExampleFetchComponent: FC<{}> = () => { +const ExampleFetchComponent = () => { return
Nothing to see yet
; }; @@ -223,7 +223,7 @@ type DenseTableProps = { viewer: Viewer; }; -export const DenseTable: FC = ({ viewer }) => { +export const DenseTable = ({ viewer }: DenseTableProps) => { const columns: TableColumn[] = [ { title: 'Name', field: 'name' }, { title: 'Created', field: 'createdAt' }, diff --git a/microsite/data/plugins/kubernetes.yaml b/microsite/data/plugins/kubernetes.yaml deleted file mode 100644 index 3134edf858..0000000000 --- a/microsite/data/plugins/kubernetes.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Kubernetes -author: Spotify -authorUrl: https://github.com/spotify -category: Kubernetes -description: Surfaces components in a Kubernetes container orchestration environment into the Backstage catalog. -documentation: https://github.com/backstage/backstage/tree/master/plugins/kubernetes -iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.png -npmPackageName: '@backstage/plugin-kubernetes' -tags: - - kubernetes - - k8s diff --git a/microsite/data/plugins/pager-duty.yaml b/microsite/data/plugins/pager-duty.yaml new file mode 100644 index 0000000000..e8a0a1b0d9 --- /dev/null +++ b/microsite/data/plugins/pager-duty.yaml @@ -0,0 +1,13 @@ +--- +title: PageDuty +author: Spotify +authorUrl: https://github.com/spotify +category: Monitoring +description: PagerDuty offers a simple way to identify any active incidents for an entity and the escalation policy. +documentation: https://github.com/backstage/backstage/tree/master/plugins/pagerduty +iconUrl: https://avatars2.githubusercontent.com/u/766800?s=200&v=4 +npmPackageName: '@backstage/plugin-pagerduty' +tags: + - monitoring + - errors + - alerting diff --git a/microsite/sidebars.json b/microsite/sidebars.json index e60d9c520e..b31f0ccdf3 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -34,6 +34,7 @@ ] } ], + "CLI": ["cli/index", "cli/commands"], "Core Features": [ { "type": "subcategory", @@ -74,6 +75,8 @@ "features/techdocs/concepts", "features/techdocs/architecture", "features/techdocs/creating-and-publishing", + "features/techdocs/configuration", + "features/techdocs/using-cloud-storage", "features/techdocs/troubleshooting", "features/techdocs/faqs" ] diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index c07285ed8c..46b2aa3470 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -48,6 +48,14 @@ h6 { color: $textColor; } +h2:hover .hash-link { + opacity: 1; +} + +.hash-link { + fill: white; +} + /* body elements */ p, ul, diff --git a/mkdocs.yml b/mkdocs.yml index 72508c821e..109be46a58 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,6 +54,8 @@ nav: - Concepts: 'features/techdocs/concepts.md' - TechDocs Architecture: 'features/techdocs/architecture.md' - Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md' + - Configuration: 'features/techdocs/configuration.md' + - Using Cloud Storage: 'features/techdocs/using-cloud-storage.md' - Troubleshooting: 'features/techdocs/troubleshooting.md' - FAQ: 'features/techdocs/FAQ.md' - Plugins: diff --git a/package.json b/package.json index 3be94c19cf..caf19b0311 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "docker-build": "yarn tsc && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", - "release": "changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}'", + "release": "changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile", "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn workspace storybook start", @@ -43,8 +43,9 @@ "version": "1.0.0", "devDependencies": { "@changesets/cli": "^2.11.0", + "@octokit/openapi-types": "^2.0.0", "@spotify/eslint-config-oss": "^1.0.1", - "@spotify/prettier-config": "^8.0.0", + "@spotify/prettier-config": "^9.0.0", "command-exists": "^1.2.9", "concurrently": "^5.2.0", "fs-extra": "^9.0.0", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 1b6e5cf1e0..799895ef56 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,105 @@ # example-app +## 0.2.7 + +### Patch Changes + +- Updated dependencies [c0fac6163] +- Updated dependencies [48c305e69] +- Updated dependencies [c911061b7] +- Updated dependencies [ab805860a] +- Updated dependencies [dae4f3983] +- Updated dependencies [8ef71ed32] +- Updated dependencies [0e6298f7e] +- Updated dependencies [f3e75508d] +- Updated dependencies [3b50f833d] +- Updated dependencies [c36a01b4c] +- Updated dependencies [c5297baeb] +- Updated dependencies [ac3560b42] +- Updated dependencies [962d1ad66] + - @backstage/plugin-org@0.3.2 + - @backstage/plugin-cost-insights@0.5.2 + - @backstage/catalog-model@0.6.0 + - @backstage/plugin-techdocs@0.5.0 + - @backstage/core@0.4.1 + - @backstage/plugin-catalog-import@0.3.2 + - @backstage/plugin-pagerduty@0.2.3 + - @backstage/cli@0.4.2 + - @backstage/plugin-sentry@0.3.1 + - @backstage/plugin-api-docs@0.4.1 + - @backstage/plugin-catalog@0.2.7 + - @backstage/plugin-circleci@0.2.5 + - @backstage/plugin-cloudbuild@0.2.5 + - @backstage/plugin-github-actions@0.2.5 + - @backstage/plugin-jenkins@0.3.4 + - @backstage/plugin-kubernetes@0.3.3 + - @backstage/plugin-lighthouse@0.2.6 + - @backstage/plugin-register-component@0.2.5 + - @backstage/plugin-rollbar@0.2.7 + - @backstage/plugin-scaffolder@0.3.4 + - @backstage/plugin-search@0.2.4 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [b2a07d2dc] +- Updated dependencies [075d3dc5a] +- Updated dependencies [e3071a0d4] +- Updated dependencies [d6e8099ed] +- Updated dependencies [88ef11b45] +- Updated dependencies [06dbe707b] +- Updated dependencies [2527628e1] +- Updated dependencies [011708102] +- Updated dependencies [04efbbdd2] +- Updated dependencies [90458fed6] +- Updated dependencies [6011b7d3e] +- Updated dependencies [79418ddb6] +- Updated dependencies [d2938af4c] +- Updated dependencies [61897fb2c] +- Updated dependencies [e1f4e24ef] +- Updated dependencies [6a0d7a9fb] +- Updated dependencies [0f8877168] +- Updated dependencies [1c69d4716] +- Updated dependencies [246799c7f] +- Updated dependencies [83b6e0c1f] +- Updated dependencies [87a33d2fe] +- Updated dependencies [1665ae8bb] +- Updated dependencies [04f26f88d] +- Updated dependencies [ff243ce96] +- Updated dependencies [2b71db211] + - @backstage/plugin-jenkins@0.3.3 + - @backstage/plugin-sentry@0.3.0 + - @backstage/plugin-cost-insights@0.5.0 + - @backstage/cli@0.4.1 + - @backstage/core@0.4.0 + - @backstage/plugin-circleci@0.2.4 + - @backstage/plugin-catalog@0.2.6 + - @backstage/plugin-pagerduty@0.2.2 + - @backstage/plugin-catalog-import@0.3.1 + - @backstage/test-utils@0.1.5 + - @backstage/plugin-search@0.2.3 + - @backstage/plugin-api-docs@0.4.0 + - @backstage/catalog-model@0.5.0 + - @backstage/plugin-techdocs@0.4.0 + - @backstage/theme@0.2.2 + - @backstage/plugin-org@0.3.1 + - @backstage/plugin-cloudbuild@0.2.4 + - @backstage/plugin-explore@0.2.2 + - @backstage/plugin-gcp-projects@0.2.2 + - @backstage/plugin-github-actions@0.2.4 + - @backstage/plugin-gitops-profiles@0.2.2 + - @backstage/plugin-graphiql@0.2.2 + - @backstage/plugin-kubernetes@0.3.2 + - @backstage/plugin-lighthouse@0.2.5 + - @backstage/plugin-newrelic@0.2.2 + - @backstage/plugin-register-component@0.2.4 + - @backstage/plugin-rollbar@0.2.6 + - @backstage/plugin-scaffolder@0.3.3 + - @backstage/plugin-tech-radar@0.3.1 + - @backstage/plugin-user-settings@0.2.3 + - @backstage/plugin-welcome@0.2.3 + ## 0.2.5 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index d46ce8b439..63710a5c28 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,40 +1,40 @@ { "name": "example-app", - "version": "0.2.5", + "version": "0.2.7", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.4.0", - "@backstage/cli": "^0.4.0", - "@backstage/core": "^0.3.2", - "@backstage/plugin-api-docs": "^0.3.1", - "@backstage/plugin-catalog": "^0.2.5", - "@backstage/plugin-catalog-import": "^0.3.0", - "@backstage/plugin-circleci": "^0.2.3", - "@backstage/plugin-cloudbuild": "^0.2.3", - "@backstage/plugin-cost-insights": "^0.4.2", - "@backstage/plugin-explore": "^0.2.1", - "@backstage/plugin-gcp-projects": "^0.2.1", - "@backstage/plugin-github-actions": "^0.2.3", - "@backstage/plugin-gitops-profiles": "^0.2.1", - "@backstage/plugin-graphiql": "^0.2.1", - "@backstage/plugin-org": "^0.3.0", - "@backstage/plugin-jenkins": "^0.3.2", - "@backstage/plugin-kubernetes": "^0.3.1", - "@backstage/plugin-lighthouse": "^0.2.4", - "@backstage/plugin-newrelic": "^0.2.1", - "@backstage/plugin-pagerduty": "0.2.1", - "@backstage/plugin-register-component": "^0.2.3", - "@backstage/plugin-rollbar": "^0.2.5", - "@backstage/plugin-scaffolder": "^0.3.2", - "@backstage/plugin-sentry": "^0.2.4", - "@backstage/plugin-search": "^0.2.2", - "@backstage/plugin-tech-radar": "^0.3.0", - "@backstage/plugin-techdocs": "^0.3.1", - "@backstage/plugin-user-settings": "^0.2.2", - "@backstage/plugin-welcome": "^0.2.2", - "@backstage/test-utils": "^0.1.4", - "@backstage/theme": "^0.2.1", + "@backstage/catalog-model": "^0.6.0", + "@backstage/cli": "^0.4.2", + "@backstage/core": "^0.4.1", + "@backstage/plugin-api-docs": "^0.4.1", + "@backstage/plugin-catalog": "^0.2.7", + "@backstage/plugin-catalog-import": "^0.3.2", + "@backstage/plugin-circleci": "^0.2.5", + "@backstage/plugin-cloudbuild": "^0.2.5", + "@backstage/plugin-cost-insights": "^0.5.2", + "@backstage/plugin-explore": "^0.2.2", + "@backstage/plugin-gcp-projects": "^0.2.2", + "@backstage/plugin-github-actions": "^0.2.5", + "@backstage/plugin-gitops-profiles": "^0.2.2", + "@backstage/plugin-graphiql": "^0.2.2", + "@backstage/plugin-org": "^0.3.2", + "@backstage/plugin-jenkins": "^0.3.4", + "@backstage/plugin-kubernetes": "^0.3.3", + "@backstage/plugin-lighthouse": "^0.2.6", + "@backstage/plugin-newrelic": "^0.2.2", + "@backstage/plugin-pagerduty": "0.2.3", + "@backstage/plugin-register-component": "^0.2.5", + "@backstage/plugin-rollbar": "^0.2.7", + "@backstage/plugin-scaffolder": "^0.3.4", + "@backstage/plugin-sentry": "^0.3.1", + "@backstage/plugin-search": "^0.2.4", + "@backstage/plugin-tech-radar": "^0.3.1", + "@backstage/plugin-techdocs": "^0.5.0", + "@backstage/plugin-user-settings": "^0.2.3", + "@backstage/plugin-welcome": "^0.2.3", + "@backstage/test-utils": "^0.1.5", + "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 28aebeeb79..627b5d19aa 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -21,7 +21,7 @@ import { SignInPage, createRouteRef, } from '@backstage/core'; -import React, { FC } from 'react'; +import React from 'react'; import Root from './components/Root'; import * as plugins from './plugins'; import { apis } from './apis'; @@ -92,7 +92,7 @@ const AppRoutes = () => ( ); -const App: FC<{}> = () => ( +const App = () => ( diff --git a/packages/app/src/components/Root/LogoFull.tsx b/packages/app/src/components/Root/LogoFull.tsx index d2b1bf1080..2fb767465b 100644 --- a/packages/app/src/components/Root/LogoFull.tsx +++ b/packages/app/src/components/Root/LogoFull.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { makeStyles } from '@material-ui/core'; const useStyles = makeStyles({ @@ -26,7 +26,7 @@ const useStyles = makeStyles({ fill: '#7df3e1', }, }); -const LogoFull: FC<{}> = () => { +const LogoFull = () => { const classes = useStyles(); return ( diff --git a/packages/app/src/components/Root/LogoIcon.tsx b/packages/app/src/components/Root/LogoIcon.tsx index d70be3dd32..507e47ddb9 100644 --- a/packages/app/src/components/Root/LogoIcon.tsx +++ b/packages/app/src/components/Root/LogoIcon.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { makeStyles } from '@material-ui/core'; const useStyles = makeStyles({ @@ -27,7 +27,7 @@ const useStyles = makeStyles({ }, }); -const LogoIcon: FC<{}> = () => { +const LogoIcon = () => { const classes = useStyles(); return ( diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 47e7d32d1a..52dd397418 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ -import React, { FC, useContext } from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext, PropsWithChildren } from 'react'; import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; @@ -55,7 +54,7 @@ const useSidebarLogoStyles = makeStyles({ }, }); -const SidebarLogo: FC<{}> = () => { +const SidebarLogo = () => { const classes = useSidebarLogoStyles(); const { isOpen } = useContext(SidebarContext); @@ -73,7 +72,7 @@ const SidebarLogo: FC<{}> = () => { ); }; -const Root: FC<{}> = ({ children }) => ( +const Root = ({ children }: PropsWithChildren<{}>) => ( @@ -102,8 +101,4 @@ const Root: FC<{}> = ({ children }) => ( ); -Root.propTypes = { - children: PropTypes.node, -}; - export default Root; diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 0c3741ec84..0e0a3074a4 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,110 @@ # @backstage/backend-common +## 0.4.1 + +### Patch Changes + +- 1d1c2860f: Implement readTree on BitBucketUrlReader and getBitbucketDownloadUrl +- 4eafdec4a: Introduce readTree method for GitLab URL Reader +- Updated dependencies [1d1c2860f] +- Updated dependencies [4eafdec4a] +- Updated dependencies [178e09323] + - @backstage/integration@0.1.4 + +## 0.4.0 + +### Minor Changes + +- 12bbd748c: Removes the Prometheus integration from `backend-common`. + + Rational behind this change is to keep the metrics integration of Backstage + generic. Instead of directly relying on Prometheus, Backstage will expose + metrics in a generic way. Integrators can then export the metrics in their + desired format. For example using Prometheus. + + To keep the existing behavior, you need to integrate Prometheus in your + backend: + + First, add a dependency on `express-prom-bundle` and `prom-client` to your backend. + + ```diff + // packages/backend/package.json + "dependencies": { + + "express-prom-bundle": "^6.1.0", + + "prom-client": "^12.0.0", + ``` + + Then, add a handler for metrics and a simple instrumentation for the endpoints. + + ```typescript + // packages/backend/src/metrics.ts + import { useHotCleanup } from '@backstage/backend-common'; + import { RequestHandler } from 'express'; + import promBundle from 'express-prom-bundle'; + import prom from 'prom-client'; + import * as url from 'url'; + + const rootRegEx = new RegExp('^/([^/]*)/.*'); + const apiRegEx = new RegExp('^/api/([^/]*)/.*'); + + export function normalizePath(req: any): string { + const path = url.parse(req.originalUrl || req.url).pathname || '/'; + + // Capture /api/ and the plugin name + if (apiRegEx.test(path)) { + return path.replace(apiRegEx, '/api/$1'); + } + + // Only the first path segment at root level + return path.replace(rootRegEx, '/$1'); + } + + /** + * Adds a /metrics endpoint, register default runtime metrics and instrument the router. + */ + export function metricsHandler(): RequestHandler { + // We can only initialize the metrics once and have to clean them up between hot reloads + useHotCleanup(module, () => prom.register.clear()); + + return promBundle({ + includeMethod: true, + includePath: true, + // Using includePath alone is problematic, as it will include path labels with high + // cardinality (e.g. path params). Instead we would have to template them. However, this + // is difficult, as every backend plugin might use different routes. Instead we only take + // the first directory of the path, to have at least an idea how each plugin performs: + normalizePath, + promClient: { collectDefaultMetrics: {} }, + }); + } + ``` + + Last, extend your router configuration with the `metricsHandler`: + + ```diff + +import { metricsHandler } from './metrics'; + + ... + + const service = createServiceBuilder(module) + .loadConfig(config) + .addRouter('', await healthcheck(healthcheckEnv)) + + .addRouter('', metricsHandler()) + .addRouter('/api', apiRouter); + ``` + + Your Prometheus metrics will be available at the `/metrics` endpoint. + +### Patch Changes + +- 38e24db00: Move the core url and auth logic to integration for the four major providers +- Updated dependencies [38e24db00] +- Updated dependencies [b8ecf6f48] +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [e3bd9fc2f] + - @backstage/integration@0.1.3 + - @backstage/config@0.1.2 + ## 0.3.3 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 13b827762c..e65edba0a5 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.3.3", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,9 +30,9 @@ }, "dependencies": { "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.1", + "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.4.0", - "@backstage/integration": "^0.1.2", + "@backstage/integration": "^0.1.4", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "archiver": "^5.0.2", @@ -43,7 +43,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", - "git-url-parse": "^11.4.0", + "git-url-parse": "^11.4.3", "helmet": "^4.0.0", "knex": "^0.21.6", "lodash": "^4.17.15", @@ -65,8 +65,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.4.0", - "@backstage/test-utils": "^0.1.4", + "@backstage/cli": "^0.4.2", + "@backstage/test-utils": "^0.1.5", "@types/archiver": "^3.1.1", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts index 6278e4ecad..3466b0f79d 100644 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -21,14 +21,6 @@ import { SingleConnectionDatabaseManager } from './SingleConnection'; jest.mock('./connection'); describe('SingleConnectionDatabaseManager', () => { - const createConfig = (data: any) => - ConfigReader.fromConfigs([ - { - context: '', - data, - }, - ]); - const defaultConfigOptions = { backend: { database: { @@ -42,7 +34,7 @@ describe('SingleConnectionDatabaseManager', () => { }, }, }; - const defaultConfig = () => createConfig(defaultConfigOptions); + const defaultConfig = () => new ConfigReader(defaultConfigOptions); // This is similar to the ts-jest `mocked` helper. const mocked = (f: Function) => f as jest.Mock; diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts index de0b0e40dc..9810ff9bef 100644 --- a/packages/backend-common/src/database/connection.test.ts +++ b/packages/backend-common/src/database/connection.test.ts @@ -18,19 +18,11 @@ import { ConfigReader } from '@backstage/config'; import { createDatabaseClient } from './connection'; describe('database connection', () => { - const createConfig = (data: any) => - ConfigReader.fromConfigs([ - { - context: '', - data, - }, - ]); - describe('createDatabaseClient', () => { it('returns a postgres connection', () => { expect( createDatabaseClient( - createConfig({ + new ConfigReader({ client: 'pg', connection: { host: 'acme', @@ -46,7 +38,7 @@ describe('database connection', () => { it('returns an sqlite connection', () => { expect( createDatabaseClient( - createConfig({ + new ConfigReader({ client: 'sqlite3', connection: ':memory:', }), @@ -57,7 +49,7 @@ describe('database connection', () => { it('tries to create a mysql connection as a passthrough', () => { expect(() => createDatabaseClient( - createConfig({ + new ConfigReader({ client: 'mysql', connection: { host: '127.0.0.1', @@ -73,7 +65,7 @@ describe('database connection', () => { it('accepts overrides', () => { expect( createDatabaseClient( - createConfig({ + new ConfigReader({ client: 'pg', connection: { host: 'acme', @@ -94,7 +86,7 @@ describe('database connection', () => { it('throws an error without a client', () => { expect(() => createDatabaseClient( - createConfig({ + new ConfigReader({ connection: '', }), ), @@ -104,7 +96,7 @@ describe('database connection', () => { it('throws an error without a connection', () => { expect(() => createDatabaseClient( - createConfig({ + new ConfigReader({ client: 'pg', }), ), diff --git a/packages/backend-common/src/database/postgres.test.ts b/packages/backend-common/src/database/postgres.test.ts index 8d139e4481..59c135f309 100644 --- a/packages/backend-common/src/database/postgres.test.ts +++ b/packages/backend-common/src/database/postgres.test.ts @@ -34,15 +34,7 @@ describe('postgres', () => { 'postgresql://foo:bar@acme:5432/foodb'; const createConfig = (connection: any): Config => - ConfigReader.fromConfigs([ - { - context: '', - data: { - client: 'pg', - connection, - }, - }, - ]); + new ConfigReader({ client: 'pg', connection }); describe('buildPgDatabaseConfig', () => { it('builds a postgres config', () => { diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/sqlite3.test.ts index a3ab331c2d..a6b8e5d84d 100644 --- a/packages/backend-common/src/database/sqlite3.test.ts +++ b/packages/backend-common/src/database/sqlite3.test.ts @@ -22,15 +22,7 @@ import { describe('sqlite3', () => { const createConfig = (connection: any) => - ConfigReader.fromConfigs([ - { - context: '', - data: { - client: 'sqlite3', - connection, - }, - }, - ]); + new ConfigReader({ client: 'sqlite3', connection }); describe('buildSqliteDatabaseConfig', () => { it('buidls a string connection', () => { diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 2c8549f917..616cbaaadc 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -158,9 +158,7 @@ describe('AzureUrlReader', () => { it('returns the wanted files from an archive', async () => { const processor = new AzureUrlReader( - { - host: 'dev.azure.com', - }, + { host: 'dev.azure.com' }, { treeResponseFactory }, ); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 1c0bd39372..e327770114 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -14,15 +14,26 @@ * limitations under the License. */ +import { ConfigReader } from '@backstage/config'; +import { msw } from '@backstage/test-utils'; +import fs from 'fs'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import path from 'path'; import { BitbucketUrlReader } from './BitbucketUrlReader'; +import { ReadTreeResponseFactory } from './tree'; + +const treeResponseFactory = ReadTreeResponseFactory.create({ + config: new ConfigReader({}), +}); describe('BitbucketUrlReader', () => { describe('implementation', () => { it('rejects unknown targets', async () => { - const processor = new BitbucketUrlReader({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }); + const processor = new BitbucketUrlReader( + { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + { treeResponseFactory }, + ); await expect( processor.read('https://not.bitbucket.com/apa'), ).rejects.toThrow( @@ -30,4 +41,141 @@ describe('BitbucketUrlReader', () => { ); }); }); + + describe('readTree', () => { + const worker = setupServer(); + msw.setupDefaultHandlers(worker); + + const repoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-repo-with-commit-hash.zip', + ), + ); + + it('returns the wanted files from an archive', async () => { + worker.use( + rest.get( + 'https://bitbucket.org/backstage/mock/get/master.zip', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + + const processor = new BitbucketUrlReader( + { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://bitbucket.org/backstage/mock/src/master', + ); + + const files = await response.files(); + + expect(files.length).toBe(2); + const indexMarkdownFile = await files[0].content(); + const mkDocsFile = await files[1].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + expect(mkDocsFile.toString()).toBe('site_name: Test\n'); + }); + + it('uses private bitbucket host', async () => { + const privateBitbucketRepoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-server-repo.zip', + ), + ); + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(privateBitbucketRepoBuffer), + ), + ), + ); + + const processor = new BitbucketUrlReader( + { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', + ); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('returns the wanted files from an archive with a subpath', async () => { + worker.use( + rest.get( + 'https://bitbucket.org/backstage/mock/get/master.zip', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + + const processor = new BitbucketUrlReader( + { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://bitbucket.org/backstage/mock/src/master/docs', + ); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + }); }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 8c97bf2ee2..4367424423 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -16,13 +16,23 @@ import { BitbucketIntegrationConfig, + getBitbucketDefaultBranch, + getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, readBitbucketIntegrationConfigs, } from '@backstage/integration'; import fetch from 'cross-fetch'; +import parseGitUri from 'git-url-parse'; +import { Readable } from 'stream'; import { NotFoundError } from '../errors'; -import { ReaderFactory, ReadTreeResponse, UrlReader } from './types'; +import { ReadTreeResponseFactory } from './tree'; +import { + ReaderFactory, + ReadTreeOptions, + ReadTreeResponse, + UrlReader, +} from './types'; /** * A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as @@ -30,19 +40,23 @@ import { ReaderFactory, ReadTreeResponse, UrlReader } from './types'; */ export class BitbucketUrlReader implements UrlReader { private readonly config: BitbucketIntegrationConfig; + private readonly treeResponseFactory: ReadTreeResponseFactory; - static factory: ReaderFactory = ({ config }) => { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const configs = readBitbucketIntegrationConfigs( config.getOptionalConfigArray('integrations.bitbucket') ?? [], ); return configs.map(provider => { - const reader = new BitbucketUrlReader(provider); + const reader = new BitbucketUrlReader(provider, { treeResponseFactory }); const predicate = (url: URL) => url.host === provider.host; return { reader, predicate }; }); }; - constructor(config: BitbucketIntegrationConfig) { + constructor( + config: BitbucketIntegrationConfig, + deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) { const { host, apiBaseUrl, token, username, appPassword } = config; if (!apiBaseUrl) { @@ -58,6 +72,7 @@ export class BitbucketUrlReader implements UrlReader { } this.config = config; + this.treeResponseFactory = deps.treeResponseFactory; } async read(url: string): Promise { @@ -82,8 +97,39 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(message); } - readTree(): Promise { - throw new Error('BitbucketUrlReader does not implement readTree'); + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const gitUrl: parseGitUri.GitUrl = parseGitUri(url); + const { name: repoName, owner: project, resource, filepath } = gitUrl; + + const isHosted = resource === 'bitbucket.org'; + + const downloadUrl = await getBitbucketDownloadUrl(url, this.config); + const response = await fetch( + downloadUrl, + getBitbucketRequestOptions(this.config), + ); + if (!response.ok) { + const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + let folderPath = `${project}-${repoName}`; + if (isHosted) { + const lastCommitShortHash = await this.getLastCommitShortHash(url); + folderPath = `${project}-${repoName}-${lastCommitShortHash}`; + } + + return this.treeResponseFactory.fromZipArchive({ + stream: (response.body as unknown) as Readable, + path: `${folderPath}/${filepath}`, + filter: options?.filter, + }); } toString() { @@ -94,4 +140,37 @@ export class BitbucketUrlReader implements UrlReader { } return `bitbucket{host=${host},authed=${authed}}`; } + + private async getLastCommitShortHash(url: string): Promise { + const { name: repoName, owner: project, ref } = parseGitUri(url); + + let branch = ref; + if (!branch) { + branch = await getBitbucketDefaultBranch(url, this.config); + } + const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`; + + const commitsResponse = await fetch( + commitsApiUrl, + getBitbucketRequestOptions(this.config), + ); + if (!commitsResponse.ok) { + const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; + if (commitsResponse.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + const commits = await commitsResponse.json(); + if ( + commits && + commits.values && + commits.values.length > 0 && + commits.values[0].hash + ) { + return commits.values[0].hash.substring(0, 12); + } + throw new Error(`Failed to read response from ${commitsApiUrl}`); + } } diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 3a76c0631b..6cc5e30dbd 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { ConfigReader } from '@backstage/config'; +import { msw } from '@backstage/test-utils'; +import fs from 'fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { ConfigReader } from '@backstage/config'; +import path from 'path'; import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; -import { msw } from '@backstage/test-utils'; import { ReadTreeResponseFactory } from './tree'; const logger = getVoidLogger(); @@ -30,105 +32,207 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ describe('GitlabUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); - beforeEach(() => { - worker.use( - rest.get('*/api/v4/projects/:name', (_, res, ctx) => - res(ctx.status(200), ctx.json({ id: 12345 })), - ), - rest.get('*', (req, res, ctx) => - res( - ctx.status(200), - ctx.json({ - url: req.url.toString(), - headers: req.headers.getAllHeaders(), - }), + describe('implementation', () => { + beforeEach(() => { + worker.use( + rest.get('*/api/v4/projects/:name', (_, res, ctx) => + res(ctx.status(200), ctx.json({ id: 12345 })), ), - ), - ); - }); - - const createConfig = (token?: string) => - new ConfigReader( - { - integrations: { gitlab: [{ host: 'gitlab.com', token }] }, - }, - 'test-config', - ); - - it.each([ - // Project URLs - { - url: - 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', - config: createConfig(), - response: expect.objectContaining({ - url: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', - headers: expect.objectContaining({ - 'private-token': '', - }), - }), - }, - { - url: - 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', - config: createConfig('0123456789'), - response: expect.objectContaining({ - url: - 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', - headers: expect.objectContaining({ - 'private-token': '0123456789', - }), - }), - }, - { - url: - 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup - config: createConfig(), - response: expect.objectContaining({ - url: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', - }), - }, - - // Raw URLs - { - url: 'https://gitlab.example.com/a/b/blob/master/c.yaml', - config: createConfig(), - response: expect.objectContaining({ - url: 'https://gitlab.example.com/a/b/raw/master/c.yaml', - }), - }, - ])('should handle happy path %#', async ({ url, config, response }) => { - const [{ reader }] = GitlabUrlReader.factory({ - config, - logger, - treeResponseFactory, + rest.get('*', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + url: req.url.toString(), + headers: req.headers.getAllHeaders(), + }), + ), + ), + ); }); - const data = await reader.read(url); - const res = await JSON.parse(data.toString('utf-8')); - expect(res).toEqual(response); - }); + const createConfig = (token?: string) => + new ConfigReader( + { + integrations: { gitlab: [{ host: 'gitlab.com', token }] }, + }, + 'test-config', + ); - it.each([ - { - url: '', - config: createConfig(''), - error: - "Invalid type in config for key 'integrations.gitlab[0].token' in 'test-config', got empty-string, wanted string", - }, - ])('should handle error path %#', async ({ url, config, error }) => { - await expect(async () => { + it.each([ + // Project URLs + { + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + headers: expect.objectContaining({ + 'private-token': '', + }), + }), + }, + { + url: + 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + config: createConfig('0123456789'), + response: expect.objectContaining({ + url: + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + headers: expect.objectContaining({ + 'private-token': '0123456789', + }), + }), + }, + { + url: + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }), + }, + + // Raw URLs + { + url: 'https://gitlab.example.com/a/b/blob/master/c.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: 'https://gitlab.example.com/a/b/raw/master/c.yaml', + }), + }, + ])('should handle happy path %#', async ({ url, config, response }) => { const [{ reader }] = GitlabUrlReader.factory({ config, logger, treeResponseFactory, }); - await reader.read(url); - }).rejects.toThrow(error); + + const data = await reader.read(url); + const res = await JSON.parse(data.toString('utf-8')); + expect(res).toEqual(response); + }); + + it.each([ + { + url: '', + config: createConfig(''), + error: + "Invalid type in config for key 'integrations.gitlab[0].token' in 'test-config', got empty-string, wanted string", + }, + ])('should handle error path %#', async ({ url, config, error }) => { + await expect(async () => { + const [{ reader }] = GitlabUrlReader.factory({ + config, + logger, + treeResponseFactory, + }); + await reader.read(url); + }).rejects.toThrow(error); + }); + }); + + describe('readTree', () => { + const repoBuffer = fs.readFileSync( + path.resolve('src', 'reading', '__fixtures__', 'repo.zip'), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://gitlab.com/backstage/mock/-/archive/repo/mock-repo.zip', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(repoBuffer), + ), + ), + ); + }); + + it('returns the wanted files from an archive', async () => { + const processor = new GitlabUrlReader( + { host: 'gitlab.com' }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://gitlab.com/backstage/mock/tree/repo', + ); + + const files = await response.files(); + expect(files.length).toBe(2); + + const indexMarkdownFile = await files[0].content(); + const mkDocsFile = await files[1].content(); + + expect(mkDocsFile.toString()).toBe('site_name: Test\n'); + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('returns the wanted files from hosted gitlab', async () => { + worker.use( + rest.get( + 'https://git.mycompany.com/backstage/mock/-/archive/repo/mock-repo.zip', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(repoBuffer), + ), + ), + ); + + const processor = new GitlabUrlReader( + { host: 'git.mycompany.com' }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://git.mycompany.com/backstage/mock/tree/repo/docs', + ); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); + + it('throws an error when branch is not specified', async () => { + const processor = new GitlabUrlReader( + { host: 'gitlab.com' }, + { treeResponseFactory }, + ); + + await expect( + processor.readTree('https://gitlab.com/backstage/mock'), + ).rejects.toThrow( + 'GitLab URL must contain a branch to be able to fetch its tree', + ); + }); + + it('returns the wanted files from an archive with a subpath', async () => { + const processor = new GitlabUrlReader( + { host: 'gitlab.com' }, + { treeResponseFactory }, + ); + + const response = await processor.readTree( + 'https://gitlab.com/backstage/mock/tree/repo/docs', + ); + + const files = await response.files(); + + expect(files.length).toBe(1); + const indexMarkdownFile = await files[0].content(); + + expect(indexMarkdownFile.toString()).toBe('# Test\n'); + }); }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index d6d1da5cbb..d51e8dc090 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -21,22 +21,37 @@ import { readGitLabIntegrationConfigs, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import { NotFoundError } from '../errors'; -import { ReaderFactory, ReadTreeResponse, UrlReader } from './types'; +import { InputError, NotFoundError } from '../errors'; +import { ReadTreeResponseFactory } from './tree'; +import { + ReaderFactory, + ReadTreeOptions, + ReadTreeResponse, + UrlReader, +} from './types'; +import parseGitUri from 'git-url-parse'; +import { Readable } from 'stream'; export class GitlabUrlReader implements UrlReader { - static factory: ReaderFactory = ({ config }) => { + private readonly treeResponseFactory: ReadTreeResponseFactory; + + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const configs = readGitLabIntegrationConfigs( config.getOptionalConfigArray('integrations.gitlab') ?? [], ); return configs.map(options => { - const reader = new GitlabUrlReader(options); + const reader = new GitlabUrlReader(options, { treeResponseFactory }); const predicate = (url: URL) => url.host === options.host; return { reader, predicate }; }); }; - constructor(private readonly options: GitLabIntegrationConfig) {} + constructor( + private readonly options: GitLabIntegrationConfig, + deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) { + this.treeResponseFactory = deps.treeResponseFactory; + } async read(url: string): Promise { const builtUrl = await getGitLabFileFetchUrl(url, this.options); @@ -59,8 +74,45 @@ export class GitlabUrlReader implements UrlReader { throw new Error(message); } - readTree(): Promise { - throw new Error('GitlabUrlReader does not implement readTree'); + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const { + name: repoName, + ref, + protocol, + resource, + full_name, + filepath, + } = parseGitUri(url); + + if (!ref) { + throw new InputError( + 'GitLab URL must contain a branch to be able to fetch its tree', + ); + } + + const archive = `${protocol}://${resource}/${full_name}/-/archive/${ref}/${repoName}-${ref}.zip`; + const response = await fetch( + archive, + getGitLabRequestOptions(this.options), + ); + if (!response.ok) { + const msg = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(msg); + } + throw new Error(msg); + } + + const path = filepath ? `${repoName}-${ref}/${filepath}/` : ''; + + return this.treeResponseFactory.fromZipArchive({ + stream: (response.body as unknown) as Readable, + path, + filter: options?.filter, + }); } toString() { diff --git a/packages/backend-common/src/reading/__fixtures__/bitbucket-repo-with-commit-hash.zip b/packages/backend-common/src/reading/__fixtures__/bitbucket-repo-with-commit-hash.zip new file mode 100644 index 0000000000..135860afd3 Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/bitbucket-repo-with-commit-hash.zip differ diff --git a/packages/backend-common/src/reading/__fixtures__/bitbucket-server-repo.zip b/packages/backend-common/src/reading/__fixtures__/bitbucket-server-repo.zip new file mode 100644 index 0000000000..be6b20d127 Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/bitbucket-server-repo.zip differ diff --git a/packages/backend-common/src/service/lib/config.test.ts b/packages/backend-common/src/service/lib/config.test.ts index 8a35f147ca..75252357d6 100644 --- a/packages/backend-common/src/service/lib/config.test.ts +++ b/packages/backend-common/src/service/lib/config.test.ts @@ -20,9 +20,7 @@ import { readCspOptions } from './config'; describe('config', () => { describe('readCspOptions', () => { it('reads valid values', () => { - const config = ConfigReader.fromConfigs([ - { context: '', data: { csp: { key: ['value'] } } }, - ]); + const config = new ConfigReader({ csp: { key: ['value'] } }); expect(readCspOptions(config)).toEqual( expect.objectContaining({ key: ['value'], @@ -31,9 +29,7 @@ describe('config', () => { }); it('accepts false', () => { - const config = ConfigReader.fromConfigs([ - { context: '', data: { csp: { key: false } } }, - ]); + const config = new ConfigReader({ csp: { key: false } }); expect(readCspOptions(config)).toEqual( expect.objectContaining({ key: false, @@ -42,9 +38,7 @@ describe('config', () => { }); it('rejects invalid value types', () => { - const config = ConfigReader.fromConfigs([ - { context: '', data: { csp: { key: [4] } } }, - ]); + const config = new ConfigReader({ csp: { key: [4] } }); expect(() => readCspOptions(config)).toThrow(/wanted string-array/); }); }); diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 838ca7d302..3f053accfa 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,64 @@ # example-backend +## 0.2.7 + +### Patch Changes + +- Updated dependencies [c6eeefa35] +- Updated dependencies [fb386b760] +- Updated dependencies [c911061b7] +- Updated dependencies [7c3ffc0cd] +- Updated dependencies [dae4f3983] +- Updated dependencies [7b15cc271] +- Updated dependencies [e7496dc3e] +- Updated dependencies [1d1c2860f] +- Updated dependencies [0e6298f7e] +- Updated dependencies [8dd0a906d] +- Updated dependencies [4eafdec4a] +- Updated dependencies [6b37c95bf] +- Updated dependencies [8c31c681c] +- Updated dependencies [7b98e7fee] +- Updated dependencies [ac3560b42] +- Updated dependencies [94c65a9d4] +- Updated dependencies [0097057ed] + - @backstage/plugin-catalog-backend@0.5.0 + - @backstage/catalog-model@0.6.0 + - @backstage/plugin-techdocs-backend@0.4.0 + - @backstage/plugin-auth-backend@0.2.7 + - @backstage/backend-common@0.4.1 + - @backstage/plugin-scaffolder-backend@0.3.5 + - example-app@0.2.7 + - @backstage/plugin-kubernetes-backend@0.2.3 + +## 0.2.6 + +### Patch Changes + +- 1e22f8e0b: Unify `dockerode` library and type dependency versions +- Updated dependencies [6e8bb3ac0] +- Updated dependencies [e708679d7] +- Updated dependencies [047c018c9] +- Updated dependencies [38e24db00] +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [12bbd748c] +- Updated dependencies [38d63fbe1] +- Updated dependencies [1e22f8e0b] +- Updated dependencies [83b6e0c1f] +- Updated dependencies [e3bd9fc2f] + - @backstage/plugin-catalog-backend@0.4.0 + - @backstage/backend-common@0.4.0 + - @backstage/config@0.1.2 + - @backstage/plugin-scaffolder-backend@0.3.4 + - @backstage/plugin-techdocs-backend@0.3.2 + - @backstage/catalog-model@0.5.0 + - example-app@0.2.6 + - @backstage/plugin-app-backend@0.3.3 + - @backstage/plugin-auth-backend@0.2.6 + - @backstage/plugin-graphql-backend@0.1.4 + - @backstage/plugin-kubernetes-backend@0.2.2 + - @backstage/plugin-proxy-backend@0.2.3 + - @backstage/plugin-rollbar-backend@0.1.5 + ## 0.2.5 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 9b0ae65eed..718b008c7b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.5", + "version": "0.2.7", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,23 +27,23 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.3.3", - "@backstage/catalog-model": "^0.4.0", - "@backstage/config": "^0.1.1", - "@backstage/plugin-app-backend": "^0.3.2", - "@backstage/plugin-auth-backend": "^0.2.5", - "@backstage/plugin-catalog-backend": "^0.3.0", - "@backstage/plugin-graphql-backend": "^0.1.3", - "@backstage/plugin-kubernetes-backend": "^0.2.1", - "@backstage/plugin-proxy-backend": "^0.2.2", - "@backstage/plugin-rollbar-backend": "^0.1.4", - "@backstage/plugin-scaffolder-backend": "^0.3.3", - "@backstage/plugin-techdocs-backend": "^0.3.1", + "@backstage/backend-common": "^0.4.1", + "@backstage/catalog-model": "^0.6.0", + "@backstage/config": "^0.1.2", + "@backstage/plugin-app-backend": "^0.3.3", + "@backstage/plugin-auth-backend": "^0.2.7", + "@backstage/plugin-catalog-backend": "^0.5.0", + "@backstage/plugin-graphql-backend": "^0.1.4", + "@backstage/plugin-kubernetes-backend": "^0.2.3", + "@backstage/plugin-proxy-backend": "^0.2.3", + "@backstage/plugin-rollbar-backend": "^0.1.5", + "@backstage/plugin-scaffolder-backend": "^0.3.5", + "@backstage/plugin-techdocs-backend": "^0.4.0", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.5", + "example-app": "^0.2.7", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -53,7 +53,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.4.0", + "@backstage/cli": "^0.4.2", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index de48280e64..4822de9c5c 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { createRouter, DirectoryPreparer, Preparers, Generators, - LocalPublish, TechdocsGenerator, CommonGitPreparer, UrlPreparer, + Publisher, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -33,10 +32,7 @@ export default async function createPlugin({ discovery, reader, }: PluginEnvironment) { - const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator(logger, config); - generators.register('techdocs', techdocsGenerator); - + // Preparers are responsible for fetching source files for documentation. const preparers = new Preparers(); const directoryPreparer = new DirectoryPreparer(logger); @@ -50,8 +46,17 @@ export default async function createPlugin({ const urlPreparer = new UrlPreparer(reader, logger); preparers.register('url', urlPreparer); - const publisher = new LocalPublish(logger, discovery); + // Generators are used for generating documentation sites. + const generators = new Generators(); + const techdocsGenerator = new TechdocsGenerator(logger, config); + generators.register('techdocs', techdocsGenerator); + // Publishers are used for + // 1. Publishing generated files to storage + // 2. Fetching files from storage and passing them to TechDocs frontend. + const publisher = Publisher.fromConfig(config, logger, discovery); + + // Docker client used by the generators. const dockerClient = new Docker(); return await createRouter({ diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 3ea3b496d7..c99f72e376 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/catalog-client +## 0.3.4 + +### Patch Changes + +- Updated dependencies [c911061b7] +- Updated dependencies [0e6298f7e] +- Updated dependencies [ac3560b42] + - @backstage/catalog-model@0.6.0 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [83b6e0c1f] +- Updated dependencies [e3bd9fc2f] + - @backstage/config@0.1.2 + - @backstage/catalog-model@0.5.0 + ## 0.3.2 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 97e2c47422..c878119a35 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.2", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.4.0", - "@backstage/config": "^0.1.1", + "@backstage/catalog-model": "^0.6.0", + "@backstage/config": "^0.1.2", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.4.0", + "@backstage/cli": "^0.4.2", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index be8e4a1d70..f6c2477d6c 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/catalog-model +## 0.6.0 + +### Minor Changes + +- ac3560b42: Remove `implementsApis` from `Component` entities. Deprecation happened in [#3449](https://github.com/backstage/backstage/pull/3449). + Use `providesApis` instead. + +### Patch Changes + +- c911061b7: Introduce a `profile` section for group entities that can optional include a + `displayName`, `email` and `picture`. +- 0e6298f7e: Ignore relations when comparing entities. This stops the refresh loop from rewriting entities over and over. + +## 0.5.0 + +### Minor Changes + +- 83b6e0c1f: Remove the deprecated fields `ancestors` and `descendants` from the `Group` entity. + + See https://github.com/backstage/backstage/issues/3049 and the PRs linked from it for details. + +### Patch Changes + +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [e3bd9fc2f] + - @backstage/config@0.1.2 + ## 0.4.0 ### Minor Changes diff --git a/packages/catalog-model/examples-relative/acme/backstage-group.yaml b/packages/catalog-model/examples-relative/acme/backstage-group.yaml index f992d155bc..1cfd469e99 100644 --- a/packages/catalog-model/examples-relative/acme/backstage-group.yaml +++ b/packages/catalog-model/examples-relative/acme/backstage-group.yaml @@ -5,6 +5,10 @@ metadata: description: The backstage sub-department spec: type: sub-department + profile: + displayName: Backstage + email: backstage@example.com + picture: https://example.com/groups/backstage.jpeg parent: infrastructure ancestors: [infrastructure, acme-corp] children: [team-a, team-b] diff --git a/packages/catalog-model/examples-relative/acme/boxoffice-group.yaml b/packages/catalog-model/examples-relative/acme/boxoffice-group.yaml index 0be1fe58cb..4bb742bdb1 100644 --- a/packages/catalog-model/examples-relative/acme/boxoffice-group.yaml +++ b/packages/catalog-model/examples-relative/acme/boxoffice-group.yaml @@ -5,6 +5,10 @@ metadata: description: The boxoffice sub-department spec: type: sub-department + profile: + displayName: Box Office + email: boxoffice@example.com + # Intentional no picture for testing parent: infrastructure ancestors: [infrastructure, acme-corp] children: [team-c, team-d] diff --git a/packages/catalog-model/examples-relative/acme/infrastructure-group.yaml b/packages/catalog-model/examples-relative/acme/infrastructure-group.yaml index 2341782944..acfeef642c 100644 --- a/packages/catalog-model/examples-relative/acme/infrastructure-group.yaml +++ b/packages/catalog-model/examples-relative/acme/infrastructure-group.yaml @@ -5,6 +5,7 @@ metadata: description: The infra department spec: type: department + # Intentional no profile for testing parent: acme-corp ancestors: [acme-corp] children: [backstage, boxoffice] diff --git a/packages/catalog-model/examples-relative/acme/org.yaml b/packages/catalog-model/examples-relative/acme/org.yaml index fe368962b1..58cce827ff 100644 --- a/packages/catalog-model/examples-relative/acme/org.yaml +++ b/packages/catalog-model/examples-relative/acme/org.yaml @@ -5,6 +5,10 @@ metadata: description: The acme-corp organization spec: type: organization + profile: + displayName: ACME Corp + email: info@example.com + picture: https://example.com/logo.jpeg ancestors: [] children: [infrastructure] descendants: diff --git a/packages/catalog-model/examples-relative/acme/team-a-group.yaml b/packages/catalog-model/examples-relative/acme/team-a-group.yaml index 8dbf0bb839..53d4bb8d80 100644 --- a/packages/catalog-model/examples-relative/acme/team-a-group.yaml +++ b/packages/catalog-model/examples-relative/acme/team-a-group.yaml @@ -5,6 +5,10 @@ metadata: description: Team A spec: type: team + profile: + # Intentional no displayName for testing + email: team-a@example.com + picture: https://example.com/groups/team-a.jpeg parent: backstage ancestors: [backstage, infrastructure, acme-corp] children: [] @@ -16,7 +20,7 @@ metadata: name: breanna.davison spec: profile: - displayName: Breanna Davison + # Intentional no displayName for testing email: breanna-davison@example.com picture: https://example.com/staff/breanna.jpeg memberOf: [team-a] diff --git a/packages/catalog-model/examples-relative/acme/team-b-group.yaml b/packages/catalog-model/examples-relative/acme/team-b-group.yaml index 00e9e41d80..a59a24317d 100644 --- a/packages/catalog-model/examples-relative/acme/team-b-group.yaml +++ b/packages/catalog-model/examples-relative/acme/team-b-group.yaml @@ -5,6 +5,10 @@ metadata: description: Team B spec: type: team + profile: + displayName: Team B + email: team-b@example.com + picture: https://example.com/groups/team-b.jpeg parent: backstage ancestors: [backstage, infrastructure, acme-corp] children: [] diff --git a/packages/catalog-model/examples-relative/acme/team-c-group.yaml b/packages/catalog-model/examples-relative/acme/team-c-group.yaml index 0f97f69cb3..31f67ac362 100644 --- a/packages/catalog-model/examples-relative/acme/team-c-group.yaml +++ b/packages/catalog-model/examples-relative/acme/team-c-group.yaml @@ -5,6 +5,10 @@ metadata: description: Team C spec: type: team + profile: + displayName: Team C + email: team-c@example.com + picture: https://example.com/groups/team-c.jpeg parent: boxoffice ancestors: [boxoffice, infrastructure, acme-corp] children: [] diff --git a/packages/catalog-model/examples-relative/acme/team-d-group.yaml b/packages/catalog-model/examples-relative/acme/team-d-group.yaml index 5a1a53a2e6..c4f87b3ac3 100644 --- a/packages/catalog-model/examples-relative/acme/team-d-group.yaml +++ b/packages/catalog-model/examples-relative/acme/team-d-group.yaml @@ -5,6 +5,10 @@ metadata: description: Team D spec: type: team + profile: + displayName: Team D + email: team-d@example.com + picture: https://example.com/groups/team-d.jpeg parent: boxoffice ancestors: [boxoffice, infrastructure, acme-corp] children: [] diff --git a/packages/catalog-model/examples/components/petstore-component.yaml b/packages/catalog-model/examples/components/petstore-component.yaml index e6dd56c274..acbb2f82b0 100644 --- a/packages/catalog-model/examples/components/petstore-component.yaml +++ b/packages/catalog-model/examples/components/petstore-component.yaml @@ -7,7 +7,7 @@ spec: type: service lifecycle: experimental owner: team-c - implementsApis: + providesApis: - petstore - streetlights - hello-world diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 1d06be8054..4fad95e122 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.4.0", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1", + "@backstage/config": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "json-schema": "^0.2.5", @@ -38,7 +38,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.4.0", + "@backstage/cli": "^0.4.2", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 2a196a65d8..ed68339a99 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -46,6 +46,10 @@ export function generateEntityEtag(): string { * are added or existing annotations were changed (since they are effectively * merged when doing updates). * + * Note that this comparison does NOT take state, relations or similar into + * account. It only compares the actual input entity data, i.e. metadata and + * spec. + * * @param previous The old state of the entity * @param next The new state of the entity */ @@ -76,6 +80,10 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { delete e1.metadata.annotations; delete e2.metadata.annotations; + // Remove things that we explicitly do not compare + delete e1.relations; + delete e2.relations; + return !lodash.isEqual(e1, e2); } diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts index 8eec63daac..030c99c151 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts @@ -33,7 +33,6 @@ describe('ComponentV1alpha1Validator', () => { type: 'service', lifecycle: 'production', owner: 'me', - implementsApis: ['api-0'], providesApis: ['api-0'], consumesApis: ['api-0'], }, @@ -104,26 +103,6 @@ describe('ComponentV1alpha1Validator', () => { await expect(validator.check(entity)).rejects.toThrow(/owner/); }); - it('accepts missing implementsApis', async () => { - delete (entity as any).spec.implementsApis; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty implementsApis', async () => { - (entity as any).spec.implementsApis = ['']; - await expect(validator.check(entity)).rejects.toThrow(/implementsApis/); - }); - - it('rejects undefined implementsApis', async () => { - (entity as any).spec.implementsApis = [undefined]; - await expect(validator.check(entity)).rejects.toThrow(/implementsApis/); - }); - - it('accepts no implementsApis', async () => { - (entity as any).spec.implementsApis = []; - await expect(validator.check(entity)).resolves.toBe(true); - }); - it('accepts missing providesApis', async () => { delete (entity as any).spec.providesApis; await expect(validator.check(entity)).resolves.toBe(true); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 680674f16e..e511dcb24a 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -29,7 +29,6 @@ const schema = yup.object>({ type: yup.string().required().min(1), lifecycle: yup.string().required().min(1), owner: yup.string().required().min(1), - implementsApis: yup.array(yup.string().required()).notRequired(), providesApis: yup.array(yup.string().required()).notRequired(), consumesApis: yup.array(yup.string().required()).notRequired(), }) @@ -43,12 +42,6 @@ export interface ComponentEntityV1alpha1 extends Entity { type: string; lifecycle: string; owner: string; - /** - * @deprecated This field will disappear on Dec 14th, 2020. Please remove - * any consuming code. The new field providesApis provides the - * same functionality like before. - */ - implementsApis?: string[]; providesApis?: string[]; consumesApis?: string[]; }; diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index 755a68cede..b25795cd31 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -28,11 +28,15 @@ describe('GroupV1alpha1Validator', () => { kind: 'Group', metadata: { name: 'doe-squad', - title: 'Doe Squad', description: 'A squad for John and Jane', }, spec: { type: 'squad', + profile: { + displayName: 'Doe Squad', + email: 'doe@doe.org', + picture: 'https://doe.org/doe', + }, parent: 'group-a', children: ['child-a', 'child-b'], }, @@ -73,6 +77,70 @@ describe('GroupV1alpha1Validator', () => { await expect(validator.check(entity)).rejects.toThrow(/type/); }); + // profile + + it('accepts missing profile', async () => { + delete (entity as any).spec.profile; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong profile', async () => { + (entity as any).spec.profile = 7; + await expect(validator.check(entity)).rejects.toThrow(/profile/); + }); + + it('profile accepts missing displayName', async () => { + delete (entity as any).spec.profile.displayName; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('profile rejects wrong displayName', async () => { + (entity as any).spec.profile.displayName = 7; + await expect(validator.check(entity)).rejects.toThrow(/displayName/); + }); + + it('profile rejects empty displayName', async () => { + (entity as any).spec.profile.displayName = ''; + await expect(validator.check(entity)).rejects.toThrow(/displayName/); + }); + + it('profile accepts missing email', async () => { + delete (entity as any).spec.profile.email; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('profile rejects wrong email', async () => { + (entity as any).spec.profile.email = 7; + await expect(validator.check(entity)).rejects.toThrow(/email/); + }); + + it('profile rejects empty email', async () => { + (entity as any).spec.profile.email = ''; + await expect(validator.check(entity)).rejects.toThrow(/email/); + }); + + it('profile accepts missing picture', async () => { + delete (entity as any).spec.profile.picture; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('profile rejects wrong picture', async () => { + (entity as any).spec.profile.picture = 7; + await expect(validator.check(entity)).rejects.toThrow(/picture/); + }); + + it('profile rejects empty picture', async () => { + (entity as any).spec.profile.picture = ''; + await expect(validator.check(entity)).rejects.toThrow(/picture/); + }); + + it('profile accepts unknown additional fields', async () => { + (entity as any).spec.profile.foo = 'data'; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + // parent + it('accepts missing parent', async () => { delete (entity as any).spec.parent; await expect(validator.check(entity)).resolves.toBe(true); @@ -83,6 +151,8 @@ describe('GroupV1alpha1Validator', () => { await expect(validator.check(entity)).rejects.toThrow(/parent/); }); + // children + it('rejects missing children', async () => { delete (entity as any).spec.children; await expect(validator.check(entity)).rejects.toThrow(/children/); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 5f73d00e66..6f2664af77 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -27,6 +27,13 @@ const schema = yup.object>({ spec: yup .object({ type: yup.string().required().min(1), + profile: yup + .object({ + displayName: yup.string().min(1).notRequired(), + email: yup.string().min(1).notRequired(), + picture: yup.string().min(1).notRequired(), + }) + .notRequired(), parent: yup.string().notRequired().min(1), // Use these manual tests because yup .required() requires at least // one element and there is no simple workaround -_- @@ -46,6 +53,11 @@ export interface GroupEntityV1alpha1 extends Entity { kind: typeof KIND; spec: { type: string; + profile?: { + displayName?: string; + email?: string; + picture?: string; + }; parent?: string; children: string[]; }; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 45214cd1e0..8f73247301 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/cli +## 0.4.2 + +### Patch Changes + +- c36a01b4c: Re-enable symlink resolution during bundling, and switch to using a resolve plugin for external linked packages. + +## 0.4.1 + +### Patch Changes + +- 06dbe707b: Update experimental backend bundle command to only output archives to `dist/` instead of a full workspace mirror in `dist-workspace/`. +- 011708102: Fixes a big in the bundling logic that caused `node_modules` inside local monorepo packages to be transformed. +- 61897fb2c: Fix config schema for `.app.listen` +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [e3bd9fc2f] + - @backstage/config@0.1.2 + ## 0.4.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index a4ddf2e519..c284b81e2e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.4.0", + "version": "0.4.2", "private": false, "publishConfig": { "access": "public" @@ -29,7 +29,7 @@ }, "dependencies": { "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.1", + "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.4.0", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", @@ -111,12 +111,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.3.3", - "@backstage/config": "^0.1.1", - "@backstage/core": "^0.3.2", - "@backstage/dev-utils": "^0.1.5", - "@backstage/test-utils": "^0.1.4", - "@backstage/theme": "^0.2.1", + "@backstage/backend-common": "^0.4.1", + "@backstage/config": "^0.1.2", + "@backstage/core": "^0.4.1", + "@backstage/dev-utils": "^0.1.6", + "@backstage/test-utils": "^0.1.5", + "@backstage/theme": "^0.2.2", "@types/diff": "^4.0.2", "@types/fs-extra": "^9.0.1", "@types/html-webpack-plugin": "^3.2.2", diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts new file mode 100644 index 0000000000..53512567eb --- /dev/null +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; + +describe('LinkedPackageResolvePlugin', () => { + it('should re-write paths for external packages', () => { + const plugin = new LinkedPackageResolvePlugin('/root/repo/node_modules', [ + { + name: 'a', + location: '/root/external-a', + }, + { + name: '@s/b', + location: '/root/external-b', + }, + ]); + + const tapAsync = jest.fn(); + const doResolve = jest.fn(); + + const resolver = { + hooks: { resolve: { tapAsync } }, + doResolve, + }; + plugin.apply(resolver); + + expect(tapAsync).toHaveBeenCalledTimes(1); + expect(tapAsync).toHaveBeenCalledWith( + 'LinkedPackageResolvePlugin', + expect.any(Function), + ); + expect(doResolve).toHaveBeenCalledTimes(0); + + // Internal module resolution is not affected + const tap = tapAsync.mock.calls[0][1]; + const callbackX = jest.fn(); + tap( + { + request: '/root/repo/package/x/src/module.ts', + path: '/root/repo/package/x/src', + context: { + issuer: '/root/repo/package/x/src/index.ts', + }, + }, + 'some-context', + callbackX, + ); + expect(callbackX).toHaveBeenCalledTimes(1); + expect(callbackX).toHaveBeenCalledWith(); + expect(doResolve).toHaveBeenCalledTimes(0); + + // Path is sometimes false + const callbackFalse = jest.fn(); + tap( + { + request: 'dummy', + path: false, + }, + 'some-context', + callbackFalse, + ); + expect(callbackFalse).toHaveBeenCalledTimes(1); + expect(callbackFalse).toHaveBeenCalledWith(); + expect(doResolve).toHaveBeenCalledTimes(0); + + // External modules have their path and issuer context rewritten, but not the request + const callbackA = jest.fn(); + tap( + { + request: '/root/external-a/src/module.ts', + path: '/root/external-a/src', + context: { + issuer: '/root/external-a/src/index.ts', + }, + }, + 'some-context', + callbackA, + ); + expect(callbackA).toHaveBeenCalledTimes(0); + expect(doResolve).toHaveBeenCalledTimes(1); + expect(doResolve).toHaveBeenCalledWith( + resolver.hooks.resolve, + { + request: '/root/external-a/src/module.ts', + path: '/root/repo/node_modules/a/src', + context: { + issuer: '/root/repo/node_modules/a/src/index.ts', + }, + }, + 'resolve /root/external-a/src/module.ts in /root/repo/node_modules/a', + 'some-context', + callbackA, + ); + + // Also handles scoped packages correctly, and issuer is not required + const callbackB = jest.fn(); + tap( + { + request: '/root/external-b/src/module.ts', + path: '/root/external-b/src', + context: { + issuer: false, + }, + }, + 'some-context', + callbackB, + ); + expect(callbackB).toHaveBeenCalledTimes(0); + expect(doResolve).toHaveBeenCalledTimes(2); + expect(doResolve).toHaveBeenLastCalledWith( + resolver.hooks.resolve, + { + request: '/root/external-b/src/module.ts', + path: '/root/repo/node_modules/@s/b/src', + context: { + issuer: false, + }, + }, + 'resolve /root/external-b/src/module.ts in /root/repo/node_modules/@s/b', + 'some-context', + callbackB, + ); + + expect(tapAsync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts new file mode 100644 index 0000000000..86269a5828 --- /dev/null +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts @@ -0,0 +1,81 @@ +/* + * 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 { resolve as resolvePath } from 'path'; +import { ResolvePlugin } from 'webpack'; +import { LernaPackage } from './types'; + +// Enables proper resolution of packages when linking in external packages. +// Without this the packages would depend on dependencies in the node_modules +// of the external packages themselves, leading to module duplication +export class LinkedPackageResolvePlugin implements ResolvePlugin { + constructor( + private readonly targetModules: string, + private readonly packages: LernaPackage[], + ) {} + + apply(resolver: any) { + resolver.hooks.resolve.tapAsync( + 'LinkedPackageResolvePlugin', + ( + data: { + request: string; + path?: false | string; + context?: { issuer?: string }; + }, + context: unknown, + callback: () => void, + ) => { + const pkg = this.packages.find( + pkg => data.path && data.path.startsWith(pkg.location), + ); + if (!pkg) { + callback(); + return; + } + + // pkg here is an external package. We rewrite the context of any imports to resolve + // from the location of the package within the node_modules of the target root rather + // than the real location of the external package. + const modulesLocation = resolvePath(this.targetModules, pkg.name); + const newContext = data.context?.issuer + ? { + ...data.context, + issuer: data.context.issuer.replace( + pkg.location, + modulesLocation, + ), + } + : data.context; + + // Re-run resolution but this time from the point of view of our target monorepo rather + // than the location of the external package. By resolving modules using this method we avoid + // pulling in e.g. `react` from the external repo, which would otherwise lead to conflicts. + resolver.doResolve( + resolver.hooks.resolve, + { + ...data, + context: newContext, + path: data.path && data.path.replace(pkg.location, modulesLocation), + }, + `resolve ${data.request} in ${modulesLocation}`, + context, + callback, + ); + }, + ); + } +} diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index b5a27dd624..3c336401ee 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -26,7 +26,8 @@ import { optimization } from './optimization'; import { Config } from '@backstage/config'; import { BundlingPaths } from './paths'; import { transforms } from './transforms'; -import { BundlingOptions, BackendBundlingOptions } from './types'; +import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; +import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types'; import { version } from '../../lib/version'; import { paths as cliPaths } from '../../lib/paths'; import { runPlain } from '../run'; @@ -70,9 +71,7 @@ async function readBuildInfo() { }; } -async function loadLernaPackages(): Promise< - { name: string; location: string }[] -> { +async function loadLernaPackages(): Promise { const LernaProject = require('@lerna/project'); const project = new LernaProject(cliPaths.targetDir); return project.getPackages(); @@ -85,12 +84,10 @@ export async function createConfig( const { checksEnabled, isDev, frontendConfig } = options; const packages = await loadLernaPackages(); - const { plugins, loaders } = transforms({ - ...options, - externalTransforms: packages.map(({ name }) => - cliPaths.resolveTargetRoot('node_modules', name), - ), - }); + const { plugins, loaders } = transforms(options); + // Any package that is part of the monorepo but outside the monorepo root dir need + // separate resolution logic. + const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root)); const baseUrl = frontendConfig.getString('app.baseUrl'); const validBaseUrl = new URL(baseUrl); @@ -165,6 +162,7 @@ export async function createConfig( extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], mainFields: ['browser', 'module', 'main'], plugins: [ + new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs), new ModuleScopePlugin( [paths.targetSrc, paths.targetDev], [paths.targetPackageJson], @@ -173,10 +171,6 @@ export async function createConfig( alias: { 'react-dom': '@hot-loader/react-dom', }, - // Enables proper resolution of packages when linking in external packages. - // Without this the packages would depend on dependencies in the node_modules - // of the external packages themselves, leading to module duplication - symlinks: false, }, module: { rules: loaders, @@ -205,13 +199,9 @@ export async function createBackendConfig( const moduleDirs = packages.map((p: any) => resolvePath(p.location, 'node_modules'), ); + const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root)); // See frontend config - const { loaders } = transforms({ - ...options, - externalTransforms: packages.map(({ name }) => - cliPaths.resolveTargetRoot('node_modules', name), - ), - }); + const { loaders } = transforms(options); return { mode: isDev ? 'development' : 'production', @@ -253,6 +243,7 @@ export async function createBackendConfig( mainFields: ['browser', 'module', 'main'], modules: [paths.rootNodeModules, ...moduleDirs], plugins: [ + new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs), new ModuleScopePlugin( [paths.targetSrc, paths.targetDev], [paths.targetPackageJson], @@ -261,7 +252,6 @@ export async function createBackendConfig( alias: { 'react-dom': '@hot-loader/react-dom', }, - symlinks: false, // See frontend config, added here for the same reason }, module: { rules: loaders, diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 19f1acff77..4f097343c2 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -25,28 +25,17 @@ type Transforms = { type TransformOptions = { isDev: boolean; - // External paths that should be transformed - externalTransforms: string[]; }; export const transforms = (options: TransformOptions): Transforms => { - const { isDev, externalTransforms } = options; + const { isDev } = options; const extraTransforms = isDev ? ['react-hot-loader'] : []; - const transformExcludeCondition = { - or: [ - // This makes sure we don't transform node_modules inside any of the local monorepo packages - /node_modules.*node_modules/, - // This excludes the local monorepo packages from the excludes, meaning they will be transformed - { and: [/node_modules/, { not: externalTransforms }] }, - ], - }; - const loaders = [ { test: /\.(tsx?)$/, - exclude: transformExcludeCondition, + exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { transforms: ['typescript', 'jsx', ...extraTransforms], @@ -55,7 +44,7 @@ export const transforms = (options: TransformOptions): Transforms => { }, { test: /\.(jsx?|mjs)$/, - exclude: transformExcludeCondition, + exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { transforms: ['jsx', ...extraTransforms], diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 71343a1761..3bd941dd0f 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -53,3 +53,8 @@ export type BackendServeOptions = BundlingPathsOptions & { checksEnabled: boolean; inspectEnabled: boolean; }; + +export type LernaPackage = { + name: string; + location: string; +}; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index e08f1650d5..5f90f2de1e 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React from 'react'; import { Typography, Grid } from '@material-ui/core'; import { InfoCard, @@ -11,7 +11,7 @@ import { } from '@backstage/core'; import ExampleFetchComponent from '../ExampleFetchComponent'; -const ExampleComponent: FC<{}> = () => ( +const ExampleComponent = () => (
diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs index 0af27a5935..8cc5ed2ab7 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Table, TableColumn, Progress } from '@backstage/core'; import Alert from '@material-ui/lab/Alert'; @@ -38,7 +38,7 @@ type DenseTableProps = { users: User[]; }; -export const DenseTable: FC = ({ users }) => { +export const DenseTable = ({ users }: DenseTableProps) => { const classes = useStyles(); const columns: TableColumn[] = [ @@ -73,7 +73,7 @@ export const DenseTable: FC = ({ users }) => { ); }; -const ExampleFetchComponent: FC<{}> = () => { +const ExampleFetchComponent = () => { const { value, loading, error } = useAsync(async (): Promise => { const response = await fetch('https://randomuser.me/api/?results=20'); const data = await response.json(); diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md new file mode 100644 index 0000000000..06a9d8e4f1 --- /dev/null +++ b/packages/config/CHANGELOG.md @@ -0,0 +1,8 @@ +# @backstage/config + +## 0.1.2 + +### Patch Changes + +- e3bd9fc2f: Fix unneeded defensive code +- e3bd9fc2f: Fix useless conditional diff --git a/packages/config/package.json b/packages/config/package.json index f1ae07ef64..4e6c2f7157 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1", + "version": "0.1.2", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index eb8c91e366..c690e0bbc9 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -69,7 +69,7 @@ export class ConfigReader implements Config { constructor( private readonly data: JsonObject | undefined, - private readonly context: string = 'empty-config', + private readonly context: string = 'mock-config', private readonly fallback?: ConfigReader, private readonly prefix: string = '', ) {} @@ -272,23 +272,17 @@ export class ConfigReader implements Config { if (value === undefined) { return this.fallback?.readConfigValue(key, validate); } - if (value !== undefined) { - const result = validate(value); - if (result !== true) { - const { - key: keyName = key, - value: theValue = value, + const result = validate(value); + if (result !== true) { + const { key: keyName = key, value: theValue = value, expected } = result; + throw new TypeError( + errors.type( + this.fullKey(keyName), + this.context, + typeOf(theValue), expected, - } = result; - throw new TypeError( - errors.type( - this.fullKey(keyName), - this.context, - typeOf(theValue), - expected, - ), - ); - } + ), + ); } return value as T; diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index 23f2ba1545..de997bf065 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/core-api +## 0.2.6 + +### Patch Changes + +- 7dd2ef7d1: Use auth provider ID to create unique session storage keys for GitHub and SAML Auth. + +## 0.2.5 + +### Patch Changes + +- b6557c098: Update ApiFactory type to correctly infer API type and disallow mismatched implementations. + + This fixes for example the following code: + + ```ts + interface MyApi { + myMethod(): void + } + + const myApiRef = createApiRef({...}); + + createApiFactory({ + api: myApiRef, + deps: {}, + // This should've caused an error, since the empty object does not fully implement MyApi + factory: () => ({}), + }) + ``` + +- d8d5a17da: Deprecated the `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types and added a new `RouteRef` type as replacement. + + Deprecated and disabled the `createSubRoute` method of `AbsoluteRouteRef`. + + Add an as of yet unused `params` option to `createRouteRef`. + +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [e1f4e24ef] +- Updated dependencies [1665ae8bb] +- Updated dependencies [e3bd9fc2f] + - @backstage/config@0.1.2 + - @backstage/test-utils@0.1.5 + - @backstage/theme@0.2.2 + ## 0.2.4 ### Patch Changes diff --git a/packages/core-api/package.json b/packages/core-api/package.json index bfff89f27a..fa8bd7a086 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.4", + "version": "0.2.6", "private": false, "publishConfig": { "access": "public", @@ -29,12 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1", - "@backstage/test-utils": "^0.1.4", - "@backstage/theme": "^0.2.1", + "@backstage/config": "^0.1.2", + "@backstage/test-utils": "^0.1.5", + "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", + "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", @@ -42,7 +43,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.0", + "@backstage/cli": "^0.4.2", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts index 2ce972af0e..459a361cf7 100644 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -16,7 +16,10 @@ import { ApiRef, createApiRef } from '../system'; import { Config } from '@backstage/config'; -// Using interface to make the ConfigApi name show up in docs +/** + * The Config API is used to provide a mechanism to access the + * runtime configuration of the system. + */ export type ConfigApi = Config; export const configApiRef: ApiRef = createApiRef({ diff --git a/packages/core-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-api/src/apis/definitions/DiscoveryApi.ts index 9777835ec8..f62a97b61a 100644 --- a/packages/core-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/core-api/src/apis/definitions/DiscoveryApi.ts @@ -31,7 +31,7 @@ export type DiscoveryApi = { /** * Returns the HTTP base backend URL for a given plugin, without a trailing slash. * - * This method must always be called just before making a request. as opposed to + * This method must always be called just before making a request, as opposed to * fetching the URL when constructing an API client. That is to ensure that more * flexible routing patterns can be supported. * diff --git a/packages/core-api/src/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts index edd6966968..dfa08130cd 100644 --- a/packages/core-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-api/src/apis/definitions/ErrorApi.ts @@ -42,10 +42,10 @@ export type ErrorContext = { * to report errors for collection by error reporting services. * * If an error can be displayed inline, e.g. as feedback in a form, that should be - * preferred over relying on this API to display the error. The main use of this api + * preferred over relying on this API to display the error. The main use of this API * for displaying errors should be for asynchronous errors, such as a failing background process. * - * Even if an error is displayed inline, it should still be reported through this api + * Even if an error is displayed inline, it should still be reported through this API * if it would be useful to collect or log it for debugging purposes, but with * the hidden flag set. For example, an error arising from form field validation * should probably not be reported, while a failed REST call would be useful to report. diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 5cfc4723e0..2d29709b02 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -30,6 +30,7 @@ export type IdentityApi = { */ getUserId(): string; + // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. /** * The profile of the signed in user. */ @@ -43,8 +44,6 @@ export type IdentityApi = { */ getIdToken(): Promise; - // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. - /** * Sign out the current user */ diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 678dce9e32..e29d1022c4 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -25,9 +25,9 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; export * from './ConfigApi'; +export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; -export * from './DiscoveryApi'; export * from './IdentityApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 812032a8a3..a7c945a907 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -84,7 +84,7 @@ class GithubAuth implements OAuthApi, SessionApi { const authSessionStore = new AuthSessionStore({ manager: sessionManager, - storageKey: 'githubSession', + storageKey: `${provider.id}Session`, sessionScopes: (session: GithubSession) => session.providerInfo.scopes, }); diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index da5f082c9b..e05f6266d6 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -63,7 +63,7 @@ class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { const authSessionStore = new AuthSessionStore({ manager: sessionManager, - storageKey: 'samlSession', + storageKey: `${provider.id}Session`, }); return new SamlAuth(authSessionStore); diff --git a/packages/core-api/src/apis/system/ApiProvider.tsx b/packages/core-api/src/apis/system/ApiProvider.tsx index f2aa70244e..91d35e5ee7 100644 --- a/packages/core-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-api/src/apis/system/ApiProvider.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import React, { FC, createContext, useContext, ReactNode } from 'react'; +import React, { + createContext, + useContext, + ReactNode, + PropsWithChildren, +} from 'react'; import PropTypes from 'prop-types'; import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { ApiAggregator } from './ApiAggregator'; @@ -26,7 +31,10 @@ type ApiProviderProps = { const Context = createContext(undefined); -export const ApiProvider: FC = ({ apis, children }) => { +export const ApiProvider = ({ + apis, + children, +}: PropsWithChildren) => { const parentHolder = useContext(Context); const holder = parentHolder ? new ApiAggregator(apis, parentHolder) : apis; @@ -62,7 +70,7 @@ export function withApis(apis: TypesToApiRefs) { return function withApisWrapper

( WrappedComponent: React.ComponentType

, ) { - const Hoc: FC> = props => { + const Hoc = (props: PropsWithChildren>) => { const apiHolder = useContext(Context); if (!apiHolder) { diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 3d32259337..3ca91966bf 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -15,10 +15,10 @@ */ import React, { ComponentType, - FC, useMemo, useState, ReactElement, + PropsWithChildren, } from 'react'; import { Route, Routes, Navigate } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; @@ -196,7 +196,7 @@ export class PrivateAppImpl implements BackstageApp { } getProvider(): ComponentType<{}> { - const Provider: FC<{}> = ({ children }) => { + const Provider = ({ children }: PropsWithChildren<{}>) => { const appThemeApi = useMemo( () => AppThemeSelector.createWithStorage(this.themes), [], @@ -233,10 +233,13 @@ export class PrivateAppImpl implements BackstageApp { } = this.components; // This wraps the sign-in page and waits for sign-in to be completed before rendering the app - const SignInPageWrapper: FC<{ + const SignInPageWrapper = ({ + component: Component, + children, + }: { component: ComponentType; children: ReactElement; - }> = ({ component: Component, children }) => { + }) => { const [result, setResult] = useState(); if (result) { @@ -247,7 +250,7 @@ export class PrivateAppImpl implements BackstageApp { return ; }; - const AppRouter: FC<{}> = ({ children }) => { + const AppRouter = ({ children }: PropsWithChildren<{}>) => { const configApi = useApi(configApiRef); let { pathname } = new URL( diff --git a/packages/core-api/src/app/AppContext.tsx b/packages/core-api/src/app/AppContext.tsx index ec3831992f..e0659d9e29 100644 --- a/packages/core-api/src/app/AppContext.tsx +++ b/packages/core-api/src/app/AppContext.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { createContext, useContext, FC } from 'react'; +import React, { createContext, PropsWithChildren, useContext } from 'react'; import { BackstageApp } from './types'; const Context = createContext(undefined); @@ -23,7 +23,10 @@ type Props = { app: BackstageApp; }; -export const AppContextProvider: FC = ({ app, children }) => ( +export const AppContextProvider = ({ + app, + children, +}: PropsWithChildren) => ( ); diff --git a/packages/core-api/src/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx index 6bbcaea93a..993de23a7a 100644 --- a/packages/core-api/src/app/AppThemeProvider.tsx +++ b/packages/core-api/src/app/AppThemeProvider.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useMemo, useEffect, useState } from 'react'; +import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; import { ThemeProvider, CssBaseline } from '@material-ui/core'; import { useApi, appThemeApiRef, AppTheme } from '../apis'; import { useObservable } from 'react-use'; @@ -68,7 +68,7 @@ const useShouldPreferDarkTheme = () => { return shouldPreferDark; }; -export const AppThemeProvider: FC<{}> = ({ children }) => { +export function AppThemeProvider({ children }: PropsWithChildren<{}>) { const appThemeApi = useApi(appThemeApiRef); const themeId = useObservable( appThemeApi.activeThemeId$(), @@ -94,4 +94,4 @@ export const AppThemeProvider: FC<{}> = ({ children }) => { {children} ); -}; +} diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 488973b664..50c4b68e43 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -17,7 +17,7 @@ import { SvgIconProps } from '@material-ui/core'; import PeopleIcon from '@material-ui/icons/People'; import PersonIcon from '@material-ui/icons/Person'; -import React, { FC } from 'react'; +import React from 'react'; import { useApp } from '../app/AppContext'; import { IconComponent, SystemIconKey, SystemIcons } from './types'; @@ -27,7 +27,7 @@ export const defaultSystemIcons: SystemIcons = { }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { - const Component: FC = props => { + const Component = (props: SvgIconProps) => { const app = useApp(); const Icon = app.getSystemIcon(key); return ; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index cb403685e1..d4eb0a0a95 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/core +## 0.4.1 + +### Patch Changes + +- 8ef71ed32: Add a `` component to `@backstage/core`. +- Updated dependencies [7dd2ef7d1] + - @backstage/core-api@0.2.6 + +## 0.4.0 + +### Minor Changes + +- ff243ce96: Introducing a new optional property within `app-config.yaml` called `auth.environment` to have configurable environment value for `auth.providers` + + **Default Value:** 'development' + + **Optional Values:** 'production' | 'development' + + **Migration-steps:** + + - To override the default value, one could simply introduce the new property `environment` within the `auth` section of the `config.yaml` + - re-run the build to reflect the changed configs + +### Patch Changes + +- 2527628e1: Link `component` prop now accepts any element type. +- 1c69d4716: Fix React warning of descendant paragraph tag +- 04f26f88d: Export the `defaultConfigLoader` implementation +- Updated dependencies [b6557c098] +- Updated dependencies [e3bd9fc2f] +- Updated dependencies [d8d5a17da] +- Updated dependencies [1665ae8bb] +- Updated dependencies [e3bd9fc2f] + - @backstage/core-api@0.2.5 + - @backstage/config@0.1.2 + - @backstage/theme@0.2.2 + ## 0.3.2 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index dd48a5d4ce..3c636a0d80 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.3.2", + "version": "0.4.1", "private": false, "publishConfig": { "access": "public", @@ -29,15 +29,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1", - "@backstage/core-api": "^0.2.4", - "@backstage/theme": "^0.2.1", + "@backstage/config": "^0.1.2", + "@backstage/core-api": "^0.2.6", + "@backstage/theme": "^0.2.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@types/dagre": "^0.7.44", "@types/react": "^16.9", "@types/react-sparklines": "^1.7.0", + "@types/prop-types": "^15.7.3", "classnames": "^2.2.6", "clsx": "^1.1.0", "d3-selection": "^2.0.0", @@ -64,8 +65,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.4.0", - "@backstage/test-utils": "^0.1.4", + "@backstage/cli": "^0.4.2", + "@backstage/test-utils": "^0.1.5", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 4a1e58db42..c99ba0ed03 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import privateExports, { AppOptions, defaultSystemIcons, @@ -93,7 +93,7 @@ export function createApp(options?: AppOptions) { const DefaultNotFoundPage = () => ( ); - const DefaultBootErrorPage: FC = ({ step, error }) => { + const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { let message = ''; if (step === 'load-config') { message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; diff --git a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx index 30940f68ac..6d6646fa18 100644 --- a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx @@ -14,16 +14,14 @@ * limitations under the License. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Snackbar, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; import { AlertMessage, useApi, alertApiRef } from '@backstage/core-api'; -type Props = {}; - // TODO: improve on this and promote to a shared component for use by all apps. -export const AlertDisplay: FC = () => { +export const AlertDisplay = () => { const [messages, setMessages] = useState>([]); const alertApi = useApi(alertApiRef); diff --git a/packages/core/src/components/Avatar/Avatar.stories.tsx b/packages/core/src/components/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000000..5ac628d72b --- /dev/null +++ b/packages/core/src/components/Avatar/Avatar.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Avatar } from './Avatar'; + +export default { + title: 'Data Display/Avatar', + component: Avatar, +}; + +export const Default = () => ( + +); + +export const NameFallback = () => ; + +export const Empty = () => ; + +export const CustomStyling = () => ( + +); diff --git a/packages/core/src/components/Avatar/Avatar.test.tsx b/packages/core/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 0000000000..da6ca8f42e --- /dev/null +++ b/packages/core/src/components/Avatar/Avatar.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { Avatar } from './Avatar'; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = render(); + + expect(getByText('JD')).toBeInTheDocument(); + }); +}); diff --git a/plugins/org/src/components/Avatar/Avatar.tsx b/packages/core/src/components/Avatar/Avatar.tsx similarity index 67% rename from plugins/org/src/components/Avatar/Avatar.tsx rename to packages/core/src/components/Avatar/Avatar.tsx index 637973ec3b..95aa4a8ced 100644 --- a/plugins/org/src/components/Avatar/Avatar.tsx +++ b/packages/core/src/components/Avatar/Avatar.tsx @@ -20,6 +20,7 @@ import { makeStyles, Theme, } from '@material-ui/core'; +import { extractInitials, stringToColor } from './utils'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -34,28 +35,13 @@ const useStyles = makeStyles((theme: Theme) => }), ); -const stringToColour = (str: string) => { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let colour = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xff; - colour += `00${value.toString(16)}`.substr(-2); - } - return colour; +export type AvatarProps = { + displayName?: string; + picture?: string; + customStyles?: CSSProperties; }; -export const Avatar = ({ - displayName, - picture, - customStyles, -}: { - displayName: string | undefined; - picture: string | undefined; - customStyles?: CSSProperties; -}) => { +export const Avatar = ({ displayName, picture, customStyles }: AvatarProps) => { const classes = useStyles(); return ( - {displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)} + {displayName && extractInitials(displayName)} ); }; diff --git a/plugins/org/src/components/Avatar/index.ts b/packages/core/src/components/Avatar/index.ts similarity index 100% rename from plugins/org/src/components/Avatar/index.ts rename to packages/core/src/components/Avatar/index.ts diff --git a/packages/core/src/components/Avatar/util.test.ts b/packages/core/src/components/Avatar/util.test.ts new file mode 100644 index 0000000000..94de957e8e --- /dev/null +++ b/packages/core/src/components/Avatar/util.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { extractInitials, stringToColor } from './utils'; + +describe('stringToColor', () => { + it('extract color', async () => { + expect(stringToColor('Jenny Doe')).toEqual('#7809fa'); + }); +}); + +describe('extractInitials', () => { + it('extract initials', async () => { + expect(extractInitials('Jenny Doe')).toEqual('JD'); + }); + + it('extract single letter for short name', async () => { + expect(extractInitials('Doe')).toEqual('D'); + }); + + it('limit the initials to two letters', async () => { + expect(extractInitials('John Jonathan Doe')).toEqual('JJ'); + }); +}); diff --git a/packages/core/src/components/Avatar/utils.ts b/packages/core/src/components/Avatar/utils.ts new file mode 100644 index 0000000000..5990a72955 --- /dev/null +++ b/packages/core/src/components/Avatar/utils.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function stringToColor(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + color += `00${value.toString(16)}`.substr(-2); + } + return color; +} + +export function extractInitials(value: string) { + return value.match(/\b\w/g)!.join('').substring(0, 2); +} diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx index cb322e5b56..9f6cdd7e7c 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useRef, useState, MouseEventHandler } from 'react'; +import React, { useRef, useState, MouseEventHandler } from 'react'; import { IconButton, makeStyles, Tooltip } from '@material-ui/core'; import PropTypes from 'prop-types'; import CopyIcon from '@material-ui/icons/FileCopy'; @@ -56,7 +56,7 @@ const defaultProps = { tooltipText: 'Text copied to clipboard', }; -export const CopyTextButton: FC = props => { +export const CopyTextButton = (props: Props) => { const { text, tooltipDelay, tooltipText } = { ...defaultProps, ...props, diff --git a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx index 9dc0d25681..64722c24b0 100644 --- a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx +++ b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx @@ -16,7 +16,7 @@ import { ClickAwayListener, makeStyles, Typography } from '@material-ui/core'; import React, { - FC, + PropsWithChildren, useCallback, useEffect, useLayoutEffect, @@ -93,12 +93,12 @@ type Placement = { textWidth: number; }; -export const FeatureCalloutCircular: FC = ({ +export const FeatureCalloutCircular = ({ featureId, title, description, children, -}) => { +}: PropsWithChildren) => { const { show, hide } = useShowCallout(featureId); const portalElement = usePortal('core.callout'); const wrapperRef = useRef(null); diff --git a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx index 4e321dfa95..073fb960e1 100644 --- a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx +++ b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { PropsWithChildren } from 'react'; import classNames from 'classnames'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; @@ -181,7 +181,7 @@ function useSmoothScroll( return setScrollTarget; } -export const HorizontalScrollGrid: FC = props => { +export const HorizontalScrollGrid = (props: PropsWithChildren) => { const { scrollStep = 100, scrollSpeed = 50, diff --git a/packages/core/src/components/Lifecycle/Lifecycle.tsx b/packages/core/src/components/Lifecycle/Lifecycle.tsx index 416df27a30..d388452cdc 100644 --- a/packages/core/src/components/Lifecycle/Lifecycle.tsx +++ b/packages/core/src/components/Lifecycle/Lifecycle.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import CSS from 'csstype'; import { makeStyles } from '@material-ui/core'; @@ -38,7 +38,7 @@ const useStyles = makeStyles({ }, }); -export const Lifecycle: FC = props => { +export const Lifecycle = (props: Props) => { const classes = useStyles(props); const { shorthand, alpha } = props; return shorthand ? ( diff --git a/packages/core/src/components/Link/Link.tsx b/packages/core/src/components/Link/Link.tsx index 59a9604fa7..b551b0511d 100644 --- a/packages/core/src/components/Link/Link.tsx +++ b/packages/core/src/components/Link/Link.tsx @@ -14,12 +14,20 @@ * limitations under the License. */ -import React, { ComponentProps } from 'react'; -import { Link as MaterialLink } from '@material-ui/core'; -import { Link as RouterLink } from 'react-router-dom'; +import React, { ElementType } from 'react'; +import { + Link as MaterialLink, + LinkProps as MaterialLinkProps, +} from '@material-ui/core'; +import { + Link as RouterLink, + LinkProps as RouterLinkProps, +} from 'react-router-dom'; -type Props = ComponentProps & - ComponentProps & { component?: React.FC }; +type Props = MaterialLinkProps & + RouterLinkProps & { + component?: ElementType; + }; /** * Thin wrapper on top of material-ui's Link component diff --git a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index cc7d99660b..cc1df3b529 100644 --- a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -22,7 +22,7 @@ import { Typography, Theme, } from '@material-ui/core'; -import React, { FC, useState } from 'react'; +import React, { useState } from 'react'; import { PendingAuthRequest } from '@backstage/core-api'; const useItemStyles = makeStyles(theme => ({ @@ -37,7 +37,7 @@ type RowProps = { setBusy: (busy: boolean) => void; }; -const LoginRequestListItem: FC = ({ request, busy, setBusy }) => { +const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => { const classes = useItemStyles(); const [error, setError] = useState(); diff --git a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx index 07078c3fa2..06b53332dc 100644 --- a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx +++ b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx @@ -24,7 +24,7 @@ import { Theme, Button, } from '@material-ui/core'; -import React, { FC, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useObservable } from 'react-use'; import LoginRequestListItem from './LoginRequestListItem'; import { useApi, oauthRequestApiRef } from '@backstage/core-api'; @@ -41,9 +41,7 @@ const useStyles = makeStyles(theme => ({ }, })); -type OAuthRequestDialogProps = {}; - -export const OAuthRequestDialog: FC = () => { +export const OAuthRequestDialog = () => { const classes = useStyles(); const [busy, setBusy] = useState(false); const oauthRequestApi = useApi(oauthRequestApiRef); diff --git a/packages/core/src/components/Progress/Progress.tsx b/packages/core/src/components/Progress/Progress.tsx index 80f4f38cc9..aacdf8821b 100644 --- a/packages/core/src/components/Progress/Progress.tsx +++ b/packages/core/src/components/Progress/Progress.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { useState, useEffect, PropsWithChildren } from 'react'; import { LinearProgress, LinearProgressProps } from '@material-ui/core'; -export const Progress: FC = props => { +export const Progress = (props: PropsWithChildren) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { diff --git a/packages/core/src/components/ProgressBars/Gauge.tsx b/packages/core/src/components/ProgressBars/Gauge.tsx index 4c339bf8e1..ca6a3a66ab 100644 --- a/packages/core/src/components/ProgressBars/Gauge.tsx +++ b/packages/core/src/components/ProgressBars/Gauge.tsx @@ -17,7 +17,7 @@ import { makeStyles, useTheme } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { Circle } from 'rc-progress'; -import React, { FC } from 'react'; +import React from 'react'; const useStyles = makeStyles(theme => ({ root: { @@ -77,7 +77,7 @@ export function getProgressColor( return palette.status.ok; } -export const Gauge: FC = props => { +export const Gauge = (props: Props) => { const classes = useStyles(props); const theme = useTheme(); const { value, fractional, inverse, unit, max } = { diff --git a/packages/core/src/components/ProgressBars/GaugeCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx index 4eb4b2e075..7f281c67a5 100644 --- a/packages/core/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { makeStyles } from '@material-ui/core'; import { InfoCard } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; @@ -36,7 +36,7 @@ const useStyles = makeStyles({ }, }); -export const GaugeCard: FC = props => { +export const GaugeCard = (props: Props) => { const classes = useStyles(props); const { title, subheader, progress, deepLink, variant } = props; diff --git a/packages/core/src/components/ProgressBars/LinearGauge.tsx b/packages/core/src/components/ProgressBars/LinearGauge.tsx index a6aea59f19..9bb7b34c09 100644 --- a/packages/core/src/components/ProgressBars/LinearGauge.tsx +++ b/packages/core/src/components/ProgressBars/LinearGauge.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { Tooltip, useTheme } from '@material-ui/core'; // @ts-ignore import { Line } from 'rc-progress'; @@ -28,7 +28,7 @@ type Props = { value: number; }; -export const LinearGauge: FC = ({ value }) => { +export const LinearGauge = ({ value }: Props) => { const theme = useTheme(); if (isNaN(value)) { return null; diff --git a/packages/core/src/components/SimpleStepper/SimpleStepper.tsx b/packages/core/src/components/SimpleStepper/SimpleStepper.tsx index 75fc1e5e62..ff65534843 100644 --- a/packages/core/src/components/SimpleStepper/SimpleStepper.tsx +++ b/packages/core/src/components/SimpleStepper/SimpleStepper.tsx @@ -16,9 +16,9 @@ import React, { Children, isValidElement, - FC, useState, useEffect, + PropsWithChildren, } from 'react'; import { Stepper as MuiStepper } from '@material-ui/core'; @@ -47,12 +47,12 @@ export interface StepperProps { activeStep?: number; } -export const SimpleStepper: FC = ({ +export const SimpleStepper = ({ children, elevated, onStepChange, activeStep = 0, -}) => { +}: PropsWithChildren) => { const [stepIndex, setStepIndex] = useState(activeStep); const [stepHistory, setStepHistory] = useState([0]); diff --git a/packages/core/src/components/SimpleStepper/SimpleStepperFooter.tsx b/packages/core/src/components/SimpleStepper/SimpleStepperFooter.tsx index c8cddb375d..a49e20913c 100644 --- a/packages/core/src/components/SimpleStepper/SimpleStepperFooter.tsx +++ b/packages/core/src/components/SimpleStepper/SimpleStepperFooter.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useContext, FC, ReactNode } from 'react'; +import React, { useContext, ReactNode, PropsWithChildren } from 'react'; import { Button, makeStyles } from '@material-ui/core'; import { StepActions } from './SimpleStepperStep'; import { VerticalStepperContext } from './SimpleStepper'; @@ -27,20 +27,33 @@ const useStyles = makeStyles(theme => ({ }, })); -export const RestartBtn: FC<{ +interface CommonBtnProps { text?: string; handleClick?: () => void; stepIndex: number; -}> = ({ text, handleClick }) => ( - -); -const NextBtn: FC<{ - text?: string; - handleClick?: () => void; +} +interface RestartBtnProps extends CommonBtnProps {} + +interface NextBtnProps extends CommonBtnProps { disabled?: boolean; last?: boolean; stepIndex: number; -}> = ({ text, handleClick, disabled, last, stepIndex }) => ( +} +interface BackBtnProps extends CommonBtnProps { + disabled?: boolean; + stepIndex: number; +} +export const RestartBtn = ({ text, handleClick }: RestartBtnProps) => ( + +); + +const NextBtn = ({ + text, + handleClick, + disabled, + last, + stepIndex, +}: NextBtnProps) => ( - {errorOccured ? ( + {errorOccurred ? (