diff --git a/.changeset/bright-plants-travel.md b/.changeset/bright-plants-travel.md new file mode 100644 index 0000000000..ad8a77d17b --- /dev/null +++ b/.changeset/bright-plants-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +The Kubernetes plugin will now re-fetch the kubernetes objects every ten seconds (not current configurable), this allows users to track the progress of deployments without refreshing the browser. diff --git a/.changeset/cold-wolves-eat.md b/.changeset/cold-wolves-eat.md new file mode 100644 index 0000000000..fa3a2180b2 --- /dev/null +++ b/.changeset/cold-wolves-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +feature: sentry-plugin allows passing search query for listing issues diff --git a/.changeset/dirty-bugs-care.md b/.changeset/dirty-bugs-care.md new file mode 100644 index 0000000000..4d90a9c939 --- /dev/null +++ b/.changeset/dirty-bugs-care.md @@ -0,0 +1,9 @@ +--- +'@backstage/test-utils': patch +'@backstage/test-utils-core': patch +--- + +Migrates all utility methods from `test-utils-core` into `test-utils` and delete exports from the old package. +This should have no impact since this package is considered internal and have no usages outside core packages. + +Notable changes are that the testing tool `msw.setupDefaultHandlers()` have been deprecated in favour of `setupRequestMockHandlers()`. diff --git a/.changeset/dull-news-visit.md b/.changeset/dull-news-visit.md new file mode 100644 index 0000000000..e1fab41ebf --- /dev/null +++ b/.changeset/dull-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Reader will now scroll to the top of the page when navigating between pages diff --git a/.changeset/friendly-olives-drop.md b/.changeset/friendly-olives-drop.md new file mode 100644 index 0000000000..371cf0e4db --- /dev/null +++ b/.changeset/friendly-olives-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Surfacing all components of the ScaffolderPage outside of the plugin so you can customize the page diff --git a/.changeset/green-parrots-thank.md b/.changeset/green-parrots-thank.md new file mode 100644 index 0000000000..9296a5119a --- /dev/null +++ b/.changeset/green-parrots-thank.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-azure-devops': patch +--- + +Azure DevOps frontend refactoring items from issue #7641 + +- Remove backend setup documentation and linked to the Azure DevOps backend plugin for these instructions +- Improved documentation to be easier to expand with new features in the future +- Removed Router based on feedback from maintainers +- Added tests for `getBuildResultComponent` and `getBuildStateComponent` from the BuildTable diff --git a/.changeset/green-tips-eat.md b/.changeset/green-tips-eat.md new file mode 100644 index 0000000000..b8c631fbf4 --- /dev/null +++ b/.changeset/green-tips-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Scaffolder: Enable back Template action buttons if template fails to execute diff --git a/.changeset/honest-drinks-eat.md b/.changeset/honest-drinks-eat.md deleted file mode 100644 index b6cb168e00..0000000000 --- a/.changeset/honest-drinks-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-graphiql': patch ---- - -Add experimental `experimentalInstallationRecipe` to `package.json`. diff --git a/.changeset/khaki-planets-prove.md b/.changeset/khaki-planets-prove.md deleted file mode 100644 index 168f30213b..0000000000 --- a/.changeset/khaki-planets-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -add a --from option to the plugin install command diff --git a/.changeset/kind-islands-attack.md b/.changeset/kind-islands-attack.md new file mode 100644 index 0000000000..a549839bc7 --- /dev/null +++ b/.changeset/kind-islands-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Added duration (startTime and finishTime) and identity (uniqueName) to the RepoBuild results. Also did a bit of refactoring to help finish up the backend items in issue #7641 diff --git a/.changeset/modern-clouds-guess.md b/.changeset/modern-clouds-guess.md deleted file mode 100644 index c316094f96..0000000000 --- a/.changeset/modern-clouds-guess.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -This continues the deprecation of classes used by the legacy catalog engine. New deprecations can be viewed in this [PR](https://github.com/backstage/backstage/pull/7500) or in the API reference documentation. - -The `batchAddOrUpdateEntities` method of the `EntitiesCatalog` interface has been marked as optional and is being deprecated. It is still implemented and required to be implemented by the legacy catalog classes, but was never implemented in the new catalog. - -This change is only relevant if you are consuming the `EntitiesCatalog` interface directly, in which case you will get a type error that you need to resolve. It can otherwise be ignored. diff --git a/.changeset/ninety-islands-compare.md b/.changeset/ninety-islands-compare.md deleted file mode 100644 index fae5b12af4..0000000000 --- a/.changeset/ninety-islands-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Bump sucrase to version 3.20.2 diff --git a/.changeset/olive-ants-allow.md b/.changeset/olive-ants-allow.md deleted file mode 100644 index 569f2825da..0000000000 --- a/.changeset/olive-ants-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Added extra configuration parameters for active directory file system identity diff --git a/.changeset/popular-apples-sparkle.md b/.changeset/popular-apples-sparkle.md new file mode 100644 index 0000000000..b8437795da --- /dev/null +++ b/.changeset/popular-apples-sparkle.md @@ -0,0 +1,26 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-model': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/errors': patch +'@backstage/search-common': patch +'@backstage/test-utils': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-search': patch +'@backstage/plugin-shortcuts': patch +--- + +Switch to use the json and observable types from `@backstage/types` diff --git a/.changeset/popular-students-sniff.md b/.changeset/popular-students-sniff.md deleted file mode 100644 index e42d5ef51b..0000000000 --- a/.changeset/popular-students-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-code-coverage-backend': patch ---- - -check for existence of lines property in files diff --git a/.changeset/quick-walls-know.md b/.changeset/quick-walls-know.md new file mode 100644 index 0000000000..14dd78364f --- /dev/null +++ b/.changeset/quick-walls-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Support optional bucketRootPath configuration parameter in S3 and GCS publishers diff --git a/.changeset/rotten-melons-carry.md b/.changeset/rotten-melons-carry.md new file mode 100644 index 0000000000..2f1429c399 --- /dev/null +++ b/.changeset/rotten-melons-carry.md @@ -0,0 +1,8 @@ +--- +'@backstage/config': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. diff --git a/.changeset/rude-starfishes-walk.md b/.changeset/rude-starfishes-walk.md new file mode 100644 index 0000000000..87ec76fd0a --- /dev/null +++ b/.changeset/rude-starfishes-walk.md @@ -0,0 +1,7 @@ +--- +'@backstage/config': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Replace usage of test-utils-core with test-utils diff --git a/.changeset/search-spicy-foxes-joke.md b/.changeset/search-spicy-foxes-joke.md new file mode 100644 index 0000000000..c1ed318f6d --- /dev/null +++ b/.changeset/search-spicy-foxes-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Handle special case when filter array has single value optimizing Lunr search behaviour. diff --git a/.changeset/spicy-goats-help.md b/.changeset/spicy-goats-help.md deleted file mode 100644 index 44f1e96332..0000000000 --- a/.changeset/spicy-goats-help.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add an experimental `install ` command. - -Given a `pluginId`, the command looks for NPM packages matching `@backstage/plugin-{pluginId}` or `backstage-plugin-{pluginId}` or `{pluginId}`. It looks for the `experimentalInstallationRecipe` in their `package.json` for the steps of installation. Detailed documentation and API Spec to follow (and to be decided as well). diff --git a/.changeset/tough-avocados-give.md b/.changeset/tough-avocados-give.md new file mode 100644 index 0000000000..08b8a7ea72 --- /dev/null +++ b/.changeset/tough-avocados-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +fix: sentry-plugin correction for allowed period values diff --git a/.changeset/unlucky-hotels-roll.md b/.changeset/unlucky-hotels-roll.md new file mode 100644 index 0000000000..0ba616c2c3 --- /dev/null +++ b/.changeset/unlucky-hotels-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Deprecated `DismissbleBannerClassKey` and fixed the typo to make `DismissableBannerClassKey` diff --git a/.changeset/wet-socks-grow.md b/.changeset/wet-socks-grow.md new file mode 100644 index 0000000000..8ccbb7a2a2 --- /dev/null +++ b/.changeset/wet-socks-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/errors': patch +--- + +Add `stringifyError` that is useful for logging e.g. `Something went wrong, ${stringifyError(e)}` diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index a38baab3dc..7e478953bc 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -8,6 +8,7 @@ apis args asciidoc async +Atlassian automations autoscaling Autoscaling @@ -310,3 +311,4 @@ Zalando Zhou zoomable zsh +Firestore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b68a96efbe..7222edef08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,7 +140,7 @@ jobs: - name: test changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} - run: yarn lerna -- run test --since origin/master -- --coverage + run: yarn lerna -- run test --since origin/master -- --coverage --runInBand env: BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} @@ -149,7 +149,7 @@ jobs: - name: test all packages (and upload coverage) if: ${{ steps.yarn-lock.outcome == 'failure' }} run: | - yarn lerna -- run test -- --coverage + yarn lerna -- run test -- --coverage --runInBand bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD) env: BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7bd73e2c61..8106dd1198 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -116,7 +116,7 @@ jobs: - name: test (and upload coverage) run: | - yarn lerna -- run test -- --coverage + yarn lerna -- run test -- --coverage --runInBand bash <(curl -s https://codecov.io/bash) # Upload code coverage for some specific flags. Also see .codecov.yml bash <(curl -s https://codecov.io/bash) -f packages/core-app-api/coverage/* -F core-app-api diff --git a/ADOPTERS.md b/ADOPTERS.md index ca09bc631c..db751bfb9b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,56 +1,61 @@ -| 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) | [@sagacity](https://github.com/sagacity) | 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. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process πŸŒ•πŸš€πŸ§‘β€πŸš€ | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | -| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | -| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | -| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | -| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | -| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| 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) | [@sagacity](https://github.com/sagacity) | 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. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process πŸŒ•πŸš€πŸ§‘β€πŸš€ | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | +| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systemsπŸš€. | +| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | +| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5242309eee..99d63cf0b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,9 +129,10 @@ In general, changesets are not needed for the documentation, build utilities, co 1. Run `yarn changeset` 2. Select which packages you want to include a changeset for 3. Select impact of change that you're introducing, using `minor` for breaking changes and `patch` otherwise. We do not use `major` changes while packages are at version `0.x`. -4. Add generated changeset to Git -5. Push the commit with your changeset to the branch associated with your PR -6. Accept our gratitude for making the release process easier on the maintainers +4. Explain your changes in the generated changeset. See [examples of well written changesets](https://backstage.io/docs/getting-started/contributors#writing-changesets). +5. Add generated changeset to Git +6. Push the commit with your changeset to the branch associated with your PR +7. Accept our gratitude for making the release process easier on the maintainers For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository. diff --git a/app-config.yaml b/app-config.yaml index c24ac1701b..8aa3bc569c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -292,6 +292,15 @@ scaffolder: visibility: public # or or 'private' auth: + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running + # the auth-backend. + # + # keyStore: + # provider: firestore + # firestore: + # projectId: my-project + # path: my-sessions + environment: development ### Providing an auth.session.secret will enable session support in the auth-backend # session: @@ -364,6 +373,11 @@ auth: development: clientId: ${AUTH_BITBUCKET_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} + atlassian: + development: + clientId: ${AUTH_ATLASSIAN_CLIENT_ID} + clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET} + scopes: ${AUTH_ATLASSIAN_SCOPES} costInsights: engineerCost: 200000 products: diff --git a/contrib/chart/backstage/files/app-config.development.yaml.tpl b/contrib/chart/backstage/files/app-config.development.yaml.tpl index 1e0a41ad63..6d3ded16c1 100644 --- a/contrib/chart/backstage/files/app-config.development.yaml.tpl +++ b/contrib/chart/backstage/files/app-config.development.yaml.tpl @@ -47,5 +47,5 @@ sentry: organization: {{ .Values.appConfig.sentry.organization | quote }} techdocs: - generators: - techdocs: 'local' + generator: + runIn: 'local' \ No newline at end of file diff --git a/contrib/chart/backstage/templates/frontend-deployment.yaml b/contrib/chart/backstage/templates/frontend-deployment.yaml index 04983513a6..dba4e2c96a 100644 --- a/contrib/chart/backstage/templates/frontend-deployment.yaml +++ b/contrib/chart/backstage/templates/frontend-deployment.yaml @@ -32,7 +32,7 @@ spec: ports: - containerPort: {{ .Values.frontend.containerPort }} resources: - {{- toYaml .Values.backend.resources | nindent 12 }} + {{- toYaml .Values.frontend.resources | nindent 12 }} envFrom: - configMapRef: name: {{ include "backstage.fullname" . }}-app-env diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index b121bab25f..6b1b8cb4e7 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -100,17 +100,16 @@ const app = createApp({ ### Backend -When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented. +When using ALB auth you can configure it as described [here](https://backstage.io/docs/auth/identity-resolver). -- replace the content of `packages/backend/plugin/auth.ts` with the below +- replace the content of `packages/backend/plugin/auth.ts` with the below and tweak it according to your needs. ```ts import { createRouter, - AuthResponse, - AuthProviderFactoryOptions, - defaultAuthProviderFactories, + createAwsAlbProvider, } from '@backstage/plugin-auth-backend'; +import { Router } from 'express'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ @@ -118,30 +117,59 @@ export default async function createPlugin({ database, config, discovery, -}: PluginEnvironment) { - const identityResolver = (payload: any): Promise> => { - return Promise.resolve({ - providerInfo: {}, - profile: { - email: payload.email, - displayName: payload.name, - picture: payload.picture, - }, - backstageIdentity: { - id: payload.email, - }, - }); - }; - const providerFactories = { - awsalb: (options: AuthProviderFactoryOptions) => - defaultAuthProviderFactories.awsalb({ ...options, identityResolver }), - }; +}: PluginEnvironment): Promise { return await createRouter({ logger, config, database, discovery, - providerFactories, + providerFactories: { + awsalb: createAwsAlbProvider({ + authHandler: async ({ fullProfile }) => { + let email: string | undefined = undefined; + if (fullProfile.emails && fullProfile.emails.length > 0) { + const [firstEmail] = fullProfile.emails; + email = firstEmail.value; + } + + let picture: string | undefined = undefined; + if (fullProfile.photos && fullProfile.photos.length > 0) { + const [firstPhoto] = fullProfile.photos; + picture = firstPhoto.value; + } + + const displayName: string | undefined = + fullProfile.displayName ?? fullProfile.username ?? fullProfile.id; + + return { + profile: { + email, + picture, + displayName, + }, + }; + }, + signIn: { + resolver: async ({ profile: { email } }, ctx) => { + const [id] = email?.split('@') ?? ''; + // Fetch from an external system that returns entity claims like: + // ['user:default/breanna.davison', ...] + const ent = [`user:default/${id}`]; + + // Resolve group membership from the Backstage catalog + const fullEnt = + await ctx.catalogIdentityClient.resolveCatalogMembership({ + entityRefs: [id].concat(ent), + logger: ctx.logger, + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, ent: fullEnt }, + }); + return { id, token }; + }, + }, + }), + }, }); } ``` diff --git a/docs/assets/getting-started/portal.png b/docs/assets/getting-started/portal.png new file mode 100644 index 0000000000..f16ad44757 Binary files /dev/null and b/docs/assets/getting-started/portal.png differ diff --git a/docs/assets/getting-started/startup.png b/docs/assets/getting-started/startup.png new file mode 100644 index 0000000000..626f7b1297 Binary files /dev/null and b/docs/assets/getting-started/startup.png differ diff --git a/docs/assets/getting-started/wizard.png b/docs/assets/getting-started/wizard.png new file mode 100644 index 0000000000..679878e252 Binary files /dev/null and b/docs/assets/getting-started/wizard.png differ diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md new file mode 100644 index 0000000000..38774ca3cc --- /dev/null +++ b/docs/auth/atlassian/provider.md @@ -0,0 +1,64 @@ +--- +id: provider +title: Atlassian Authentication Provider +sidebar_label: Atlassian +description: Adding Atlassian as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with an Atlassian authentication +provider that can authenticate users using Atlassian products. This auth +**only** provides scopes for the following APIs: + +- Confluence API +- User REST API +- Jira platform REST API +- Jira Service Desk API +- Personal data reporting API +- User identity API + +## Create an OAuth 2.0 (3LO) app in the Atlassian developer console + +To add Atlassian authentication, you must create an OAuth 2.0 (3LO) app. + +Go to `https://developer.atlassian.com/console/myapps/`. + +Click on the drop down `Create`, and choose `OAuth 2.0 integration`. + +Name your integration and click on the `Create` button. + +Settings for local development: + +- Callback URL: `http://localhost:7000/api/auth/atlassian` +- Use rotating refresh tokens +- For permissions, you **must** enable `View user profile` for the currently + logged-in user, under `User identity API` + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the +root `auth` configuration: + +```yaml +auth: + environment: development + providers: + atlassian: + development: + clientId: ${AUTH_ATLASSIAN_CLIENT_ID} + clientSecret: ${AUTH_ATLASSIAN_CLIENT_SECRET} + scopes: ${AUTH_ATLASSIAN_SCOPES} +``` + +The Atlassian provider is a structure with three configuration keys: + +- `clientId`: The Key you generated in the developer console. +- `clientSecret`: The Secret tied to the generated Key. +- `scopes`: List of scopes the app has permissions for, separated by spaces. + +**NOTE:** the scopes `offline_access` and `read:me` are provided by default. + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `atlassianAuthApi` reference and +`SignInPage` component as shown in +[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index ba32706cc4..7230f5cf16 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -76,7 +76,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml"] For more details on how the `backend:bundle` command and the `skeleton.tar.gz` file works, see the -[`backend:bundle` command docs](../cli/commands.md#backendbundle). +[`backend:bundle` command docs](../local-dev/cli-commands.md#backendbundle). The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be executed with the root of the repo as the build context, in order to get access diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index f48faa202d..6b3991144b 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -29,6 +29,7 @@ kubernetes: serviceAccountToken: ${K8S_MINIKUBE_TOKEN} dashboardUrl: http://127.0.0.1:64713 # url copied from running the command: minikube service kubernetes-dashboard -n kubernetes-dashboard dashboardApp: standard + caData: ${K8S_CONFIG_CA_DATA} - url: http://127.0.0.2:9999 name: aws-cluster-1 authProvider: 'aws' @@ -135,6 +136,24 @@ See also https://github.com/backstage/backstage/tree/master/plugins/kubernetes/src/utils/clusterLinks/formatters for real examples. +##### `clusters.\*.caData` (optional) + +PEM-encoded certificate authority certificates. + +This values could be obtained via inspecting the Kubernetes config file (usually +at `~/.kube/config`) under `clusters.cluster.certificate-authority-data`. For +GKE, execute the following command to obtain the value + +``` +gcloud container clusters describe \ + --zone= \ + --format="value(masterAuth.clusterCaCertificate)" +``` + +See also +https://cloud.google.com/kubernetes-engine/docs/how-to/api-server-authentication#environments-without-gcloud +for complete docs about GKE without `gcloud`. + #### `gke` This cluster locator is designed to work with Kubernetes clusters running in diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 36bb724be6..2e0d9dccb9 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -59,14 +59,18 @@ add the following: ```typescript // In packages/backend/src/plugins/kubernetes.ts -import { createRouter } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, config, }: PluginEnvironment) { - return await createRouter({ logger, config }); + const { router } = await KubernetesBuilder.createBuilder({ + logger, + config, + }).build(); + return router; } ``` diff --git a/docs/features/search/README.md b/docs/features/search/README.md index 9f5a5d7009..0856451882 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -102,20 +102,24 @@ more to come... See [Backstage Search Architecture](architecture.md) to get an overview of how the search engines are used. -| Search Engine | Support Status | -| ------------- | -------------- | -| Basic (lunr) | βœ… | -| ElasticSearch | Not yet ❌ | +| Search Engines | Support Status | +| -------------------------------------------------- | -------------- | +| [Lunr](./search-engines.md#lunr) | βœ… | +| [ElasticSearch](./search-engines.md#elasticsearch) | βœ… | +| [Postgres](./search-engines.md#postgres) | βœ… | -[Reach out to us](#feedback) if you want to chat about support for more search -engines. +[Reach out to us](#get-involved) if you want to chat about support for more +search engines. ## Plugins Integrated with Search -| Plugin | Support Status | -| -------- | -------------- | -| Catalog | βœ… | -| TechDocs | Not yet ❌ | +| Plugin | Support Status | +| -------------------------------------------------------------- | -------------- | +| Catalog | βœ… | +| [TechDocs](./how-to-guides.md#how-to-index-techdocs-documents) | βœ… | + +[Reach out to us](#get-involved) if you want to chat about support for more +plugins integrated to search. ## Tech Stack diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md new file mode 100644 index 0000000000..621d46b51e --- /dev/null +++ b/docs/features/search/how-to-guides.md @@ -0,0 +1,98 @@ +--- +id: how-to-guides +title: Search "HOW TO" guides +sidebar_label: "HOW TO" guides +description: Search "HOW TO" guides +--- + +## How to implement your own Search API + +The Search plugin provides implementation of one primary API by default: the +[SearchApi](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L35), +which is responsible for talking to the search-backend to query search results. + +There may be occasions where you need to implement this API yourself, to +customize it to your own needs - for example if you have your own search backend +that you want to talk to. The purpose of this guide is to walk you through how +to do that in two steps. + +1. Implement the `SearchApi` + [interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31) + according to your needs. + +```typescript +export class SearchClient implements SearchApi { + // your implementation +} +``` + +2. Override the API ref `searchApiRef` with your new implemented API in the + `App.tsx` using `ApiFactories`. + [Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis). + +```typescript +const app = createApp({ + apis: [ + // SearchApi + createApiFactory({ + api: searchApiRef, + deps: { discovery: discoveryApiRef }, + factory({ discovery }) { + return new SearchClient({ discoveryApi: discovery }); + }, + }), + ], +}); +``` + +## How to index TechDocs documents + +The TechDocs plugin has supported integrations to Search, meaning that it +provides a default collator ready to be used. + +The purpose of this guide is to walk you through how to register the +[DefaultTechDocsCollator](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts) +in your App, so that you can get TechDocs documents indexed. + +If you have been through the +[Getting Started with Search guide](https://backstage.io/docs/features/search/getting-started), +you should have the `packages/backend/src/plugins/search.ts` file available. If +so, you can go ahead and follow this guide - if not, start by going through the +getting started guide. + +1. Import the DefaultTechDocsCollator from `@backstage/plugin-techdocs-backend`. + +```typescript +import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +``` + +2. Register the DefaultTechDocsCollator with the IndexBuilder. + +```typescript +indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: DefaultTechDocsCollator.fromConfig(config, { + discovery, + logger, + }), +}); +``` + +You should now have your TechDocs documents indexed to your search engine of +choice! + +If you want your users to be able to filter down to the techdocs type when +searching, you can update your `SearchPage.tsx` file in +`packages/app/src/components/search` by adding `techdocs` to the list of values +of the `SearchType` component. + +```tsx + + + ... + +``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 7a9aa099e9..c174b94557 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -620,9 +620,6 @@ The following describes the following entity kind: | `apiVersion` | `backstage.io/v1beta2` | | `kind` | `Template` | -If you're looking for docs on `v1alpha1` you can find them -[here](../software-templates/legacy.md) - A template definition describes both the parameters that are rendered in the frontend part of the scaffolding wizard, and the steps that are executed when scaffolding that component. diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index ed106bb5dd..340daae7d3 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -429,3 +429,101 @@ from one environment to the other, do rollbacks, see their relative performance metrics, and similar. This coherency and collection of tooling in one place is where something like Backstage can offer the most value and effectiveness of use. Splitting your entities apart into small islands makes this harder. + +## Implementing custom model extensions + +This section walks you through the steps involved extending the catalog model +with a new Entity type. + +### Creating a custom entity definition + +The first step of introducing a custom entity is to define what shape and schema +it has. We do this both using a TypeScript type, along with a JSONSchema schema. + +Most of the time you will want to have at least the TypeScript type of your +extension available in both frontend and backend code, which means you likely +want to have an isomorphic package that houses these types. Within the Backstage +main repo the package naming pattern of `-common` is used for isomorphic +packages, and you may choose to adopt this pattern as well. + +There's at this point no existing templates for generating isomorphic plugins +using the `@backstage/cli`. Perhaps the simplest wat to get started right now is +to copy the contents of one of the existing packages in the main repository, +such as `plugins/scaffolder-common`, and rename the folder and file contents to +the desired name. This example uses _foobar_ as the plugin name so the plugin +will be named _foobar-common_. + +Once you have a common package in place you can start adding your own entity +definitions. For the exact details on how to do that we defer to getting +inspired by the existing +[scaffolder-common](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-common/src/index.ts) +package. But in short you will need to declare a TypeScript type and a +JSONSchema for the new entity kind. + +### Building a custom processor for the entity + +The next step is to create a custom processor for your new entity kind. This +will be used within the catalog to make sure that it's able to ingest and +validate entities of our new kind. Just like with the definition package, you +can find inspiration in for example the existing +[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts). +We also provide a high-level example of what a catalog process for a custom +entity might look like: + +```ts +import { entityKindSchemaValidator } from '@backstage/catalog-model'; + +export class FoobarEntitiesProcessor implements CatalogProcessor { + // You often end up wanting to support multiple versions of your kind as you + // iterate on the definition, so we keep each version inside this array. + private readonly validators = [ + // This is where we use the JSONSchema that we export from our isomorphic package + entityKindSchemaValidator(foobarEntityV1alpha1Schema), + ]; + + // validateEntityKind is responsible for signaling to the catalog processing engine + // that this entity is valid and should therefore be submitted for further processing. + async validateEntityKind(entity: Entity): Promise { + for (const validator of this.validators) { + if (validator(entity)) { + return true; + } + } + + return false; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise { + if ( + entity.apiVersion === 'example.com/v1alpha1' && + entity.kind === 'Foobar' + ) { + const foobarEntity = entity as FoobarEntityV1alpha1; + + // Here we can modify the entity or emit results related to the entity + // Typically you will want to emit any relations associated with the entity here + emit(results.relation({ ... })) + } + + return entity; + } +} +``` + +Once the processor is created it can be wired up to the catalog via the +`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`: + +```diff ++ import { FoobarEntitiesProcessor implements CatalogProcessor { + } from '@internal/plugin-foobar-backend'; + + // ... + + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor(new FoobarEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); +``` diff --git a/docs/features/software-templates/legacy.md b/docs/features/software-templates/legacy.md deleted file mode 100644 index 49f3e21904..0000000000 --- a/docs/features/software-templates/legacy.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -id: template-legacy -title: Writing Templates (Legacy) -# prettier-ignore -description: Old documentation describing the backstage.io/v1alpha1 format of the Template Schema ---- - -## Kind: Template - -Describes the following entity kind: - -| Field | Value | -| ------------ | ----------------------- | -| `apiVersion` | `backstage.io/v1alpha1` | -| `kind` | `Template` | - -A Template describes a skeleton for use with the Scaffolder. It is used for -describing what templating library is supported, and also for documenting the -variables that the template requires using -[JSON Forms Schema](https://jsonforms.io/). - -Descriptor files for this kind may look as follows. - -```yaml -apiVersion: backstage.io/v1alpha1 -kind: Template -metadata: - name: react-ssr-template - title: React SSR Template - description: - Next.js application skeleton for creating isomorphic web applications. - tags: - - recommended - - react -spec: - owner: web@example.com - templater: cookiecutter - type: website - path: '.' - schema: - required: - - component_id - - description - properties: - component_id: - title: Name - type: string - description: Unique name of the component - description: - title: Description - type: string - description: Description of the component -``` - -In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) -shape, this kind has the following structure. - -### `apiVersion` and `kind` [required] - -Exactly equal to `backstage.io/v1alpha1` and `Template`, respectively. - -### `metadata.title` [required] - -The nice display name for the template as a string, e.g. `React SSR Template`. -This field is required as is used to reference the template to the user instead -of the `metadata.name` field. - -### `metadata.tags` [optional] - -A list of strings that can be associated with the template, e.g. -`['recommended', 'react']`. - -This list will also be used in the frontend to display to the user so you can -potentially search and group templates by these tags. - -### `spec.type` [optional] - -The type of component as a string, e.g. `website`. This field is optional but -recommended. - -The software catalog accepts any type value, but an organization should take -great care to establish a proper taxonomy for these. Tools including Backstage -itself may read this field and behave differently depending on its value. For -example, a website type component may present tooling in the Backstage interface -that is specific to just websites. - -The current set of well-known and common values for this field is: - -- `service` - a backend service, typically exposing an API -- `website` - a website -- `library` - a software library, such as an npm module or a Java library - -### `spec.templater` [required] - -The templating library that is supported by the template skeleton as a string, -e.g `cookiecutter`. - -Different skeletons will use different templating syntax, so it's common that -the template will need to be run with a particular piece of software. - -This key will be used to identify the correct templater which is registered into -the `TemplatersBuilder`. - -The values which are available by default are: - -- `cookiecutter` - [cookiecutter](https://github.com/cookiecutter/cookiecutter). - -### `spec.path` [optional] - -The string location where the templater should be run if it is not on the same -level as the `template.yaml` definition, e.g. `./cookiecutter/skeleton`. - -This will set the `cwd` when running the templater to the folder path that you -specify relative to the `template.yaml` definition. - -This is also particularly useful when you have multiple template definitions in -the same repository but only a single `template.yaml` registered in backstage. diff --git a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md deleted file mode 100644 index 58d88e8e9e..0000000000 --- a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md +++ /dev/null @@ -1,333 +0,0 @@ ---- -id: migrating-from-v1alpha1-to-v1beta2 -title: Migrating to v1beta2 templates -# prettier-ignore -description: How to move your old templates from v1alpha1 to the more declarative v1beta2 ---- - -# What's new? - -Previously, the scaffolder was very restricted in what you could do when -creating new software components from templates. There were three scaffolding -steps which was pretty hard to extend and add new functionality to, difficult to -re-use logic between templates. There used to be a fixed pipeline of -`preparers`, `templaters`, and `publishers`, which were defined by the backend -and needed to be run for each template. This is now changed, to give the -template total control over what should be executed as part of the templating -run. This makes templates a little more declarative as you can now register -different `actions` or `functions` with the `scaffolder-backend` which you then -can decide how, and in what order, to run using the template definition YAML -file. - -We've also made some improvements, and added some helpers to work with -cookiecutter. The skeleton for a template can now be stored in a different place -to where your entity definition is: previously you needed to have your -`template.yaml` next to the skeleton source (`{{cookiecutter.component_id}}` -directory), but now that's not the case. Part of the changes with the `v1beta2` -syntax is that you can grab your template source from any repository, and re-use -them between templates. - -We've also renamed the `schema` property to `parameters` as this makes more -sense when using them as parameters to the actions or steps that you've setup -for your templates. There's the added benefit that you can now assign an array -to the `parameters` property, which will then give you multiple steps in the UI, -so you can split apart your input parameters and group them as needed rather -than having one long list of input fields. - -## The `parameters` property - -The `schema` key has now been renamed to `parameters` with a few more features. -You can pass an array now to break apart the input form into different steps in -the UI. You can also specify `ui:schema` fields that are passed along to -[`react-jsonschema-form`](https://rjsf-team.github.io/react-jsonschema-form/) -inline with the JSON schema. - -```yaml -spec: - parameters: - - title: Fill in some steps - required: - - name - properties: - name: - title: Name - type: string - description: Unique name of the component - ui:autofocus: true - ui:options: - rows: 5 -``` - -## The `steps` property - -`v1beta2` template syntax introduces the new `steps` property, which is an array -of `actions` that the scaffolder will run in combination with the user input -that is declared in the `schema`. Actions look like the following: - -```yaml -spec: - steps: - - id: publish # a unique id for the step, can be anything you like - name: Publish # a user friendly name for the step, this is what is shown in the frontend - action: publish:github # the action ID that has been registered with the scaffolder-backend - input: # parameters that are passed as input to the action handler function - allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' # handlebars templating is supported with the values from the parameters section in the same file. - repoUrl: '{{ parameters.repoUrl }}' -``` - -# Migrating a `v1alpha1` template - -## The template definition (.yaml) - -### `parameters` - -Because of the changes to invert the control to the `template.yaml` definition -for running the workflow, we need to adjust the `schema` property and we also -now need to define what the template is actually going to do as part of the -template run. - -A simple migration would move the following yaml: - -```yaml -apiVersion: backstage.io/v1alpha1 -kind: Template -metadata: - name: react-ssr-template - title: React SSR Template - description: Create a website powered with Next.js - tags: - - recommended - - react -spec: - owner: web@example.com - templater: cookiecutter - type: website - path: '.' - schema: - required: - - component_id - - description - properties: - component_id: - title: Name - type: string - description: Unique name of the component - description: - title: Description - type: string - description: Help others understand what this website is for. -``` - -To something that looks like the following: - -```yaml -apiVersion: backstage.io/v1beta2 -kind: Template -metadata: - name: react-ssr-template - title: React SSR Template - description: Create a website powered with Next.js - tags: - - recommended - - react -spec: - owner: web@example.com - type: website - parameters: - - title: Add some input - required: - - component_id - - description - properties: - component_id: - title: Name - type: string - description: Unique name of the component - description: - title: Description - type: string - description: Help others understand what this website is for. - - title: Some more additional info that was previously provided automatically - required: - - owner - - repoUrl - properties: - owner: - title: Owner - type: string - description: Owner of the component - ui:field: OwnerPicker - ui:options: - allowedKinds: - - Group - - title: Choose a location - repoUrl: - title: Repository Location - type: string - ui:field: RepoUrlPicker - ui:options: - allowedHosts: - - github.com -``` - -There are a few things to note here. On the `alpha` version, the second step of -the template flow in the frontend was provided by Backstage for free, so we used -to collect the user input for the `owner` field and the `repositoryUrl` that you -were going to publish to. Now because `actions` can have any workflow they like, -it doesn't make sense to still provide these fields for every scaffolding -workflow, as you might not need these anymore. That's why we now manually add -those fields back into the template parameters that are shown to the user: - -```yaml - - title: Some more additional info that was previously provided automatically - required: - - owner - - repoUrl - properties: - owner: - title: Owner - type: string - description: Owner of the component - ui:field: OwnerPicker - ui:options: - allowedKinds: - - Group - - title: Choose a location - repoUrl: - title: Repository Location - type: string - ui:field: RepoUrlPicker - ui:options: - allowedHosts: - - github.com -``` - -Maybe you also don't need to publish to `github.com`, you should replace this -with your VCS provider URL that is listed in your `integrations` config instead. - -### `steps` - -So now we should have all the required information that we need from the user in -a much more extensible way. We now need to tell the scaffolder what to do with -these parameters and what to do with the user input. - -We've made templating using `cookiecutter` a little simpler. You don't need to -store the `cookiecutter` skeleton in the same directory as the `template.yaml` -definition, it can live wherever you like - maybe a shared repository somewhere -so you can re-use the skeletons but apply different actions for different -templates depending on your use case. - -We also no longer need to have a directory called -`{{cookiecutter.component_id}}`. This is because now we can't ensure that -`component_id` will be a parameter that is provided from the frontend, this -could break `cookiecutter`. If your directory structure used to look like this: - -``` -my-awesome-template - -> {{cookiecutter.component_id}} - -> file.txt - -> some_more_files.ts - -> hooks - -> post_gen_project.sh - -> template.yaml -``` - -We now recommend that you move to the following structure: - -``` -my-awesome-template - -> skeleton - -> file.txt - -> some_more_files.ts - -> template.yaml -``` - -This migration renames the skeleton folder to something more semantic, and also -drops support for `cookiecutter` hooks. We've dropped support for `cookiecutter` -hooks for now, as hopefully everything that is stored in these hooks can be -moved to `actions` instead, and for security reasons, it's more secure to run -trusted code that you ship with Backstage as an action rather than some script -that can be pulled in from anywhere which doesn't get vetted first. It's a -pretty big security risk that those scripts will be run on Backstage instances -inside your infrastructure, especially `.sh` files. - -If you really need hooks and can't find a suitable solution by using actions -please reach out to us through a ticket and we'll see what we can do to assist -:) - -You'll notice that we removed the `templater` property from the `spec` -definition in the template `yaml`, so there's no way to define that this is a -`cookiecutter` `templater`. - -We've created a built-in action that you can use which will when run, go grab a -directory from anywhere and run `cookiecutter` on top of it, and then extract -the contents into the working directory for the scaffolder. - -Adding the `steps` for a simple template should look something like the -following: - -```yaml -spec: - steps: - # this action will go use cookiecutter to template some files into the working directory - - id: template # an ID for the templating step - name: Create skeleton # A user friendly name for the action - action: fetch:cookiecutter - input: - url: ./skeleton # this is the directory for your skeleton files. - # If it's located next to the `template.yaml` then you can use a relative path, - # otherwise you can use absolute URLs that point at the VCS: https://github.com/backstage/backstage/tree/master/some_folder_somewhere - values: - # for each value that you need to pass to cookiecutter, they should be listed here and set in this values object. - # You can use the handlebars templating syntax to pull them from the input parameters listed in the same file - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' - destination: '{{ parseRepoUrl parameters.repoUrl }}' - - # this action is for publishing the working directory to the VCS - - id: publish - name: Publish - action: publish:github - input: - allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' - - # this action will then register the created component in Backstage - - id: register - name: Register - action: catalog:register - input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' - catalogInfoPath: '/catalog-info.yaml' -``` - -### `output` - -Steps can output values, and so can the template itself. This is good for -returning values to the frontend, so we can make the buttons like -`Go to catalog` and `Go to repo` work correctly. You can add the following to -your `template.yaml` to make sure you return the right values from the steps: - -```yaml -spec: - output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' -``` - -Or you can return a `links` array with text and a URL explicitly: - -```yaml -spec: - output: - links: - - url: '{{steps.publish.output.remoteUrl}}' - title: 'Go to Repo' -``` - -## Questions? - -If you have any questions or feedback, please reach out to us on GitHub or -Discord and we will do our best to help! diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md new file mode 100644 index 0000000000..280eab083d --- /dev/null +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -0,0 +1,156 @@ +--- +id: migrating-from-v1beta2-to-v1beta3 +title: Migrating to v1beta3 templates +# prettier-ignore +description: How to migrate your existing templates to beta3 syntax +--- + +# What's new? + +Well then, here we are! πŸš€ + +Backstage has had many forms of templating languages throughout different +plugins and different systems. We've had `cookiecutter` syntax in templates, and +we also had `handlebars` templating in the `kind: Template`. Then we wanted to +remove the additional dependency on `cookiecutter` for Software Templates out of +the box, so we introduced `nunjucks` as an alternative in `fetch:template` +action which is based on the `jinja2` syntax so they're pretty similar. In an +effort to reduce confusion and unify on to one templating language, we're +officially deprecating support for `handlebars` templating in the +`kind: Template` entities with `apiVersion` `scaffolder.backstage.io/v1beta3` +and moving to using `nunjucks` instead. + +This provides us a lot of built in `filters` (`handlebars` helpers), that as +Template authors will give you much more flexibility out of the box, and also +open up sharing of filters in the Entity and the actual `skeleton` too, and +removing the slight differences between the two languages. + +We've also removed a lot of the built in helpers that we shipped with +`handlebars`, as they're now supported as first class citizens by either +`nunjucks` or the new `scaffolder` when using `scaffolder.backstage.io/v1beta3` +`apiVersion` + +The migration path is pretty simple, and we've removed some of the pain points +from writing the `handlebars` templates too. Let's go through what's new and how +to upgrade. + +## `backstage.io/v1beta2` -> `scaffolder.backstage.io/v1beta3` + +The most important change is that you'll need to switch over the `apiVersion` in +your templates to the new one. + +```diff + kind: Template +- apiVersion: backstage.io/v1beta2 ++ apiVersion: scaffolder.backstage.io/v1beta3 +``` + +## `${{ }}` instead of `"{{ }}"` + +One really big readability issue and cause for confusion was the fact that with +`handlebars` and `yaml` you always had to wrap your templating strings in quotes +in `yaml` so that it didn't try to parse it as a `json` object and fail. This +was pretty annoying, as it also meant that all things look like strings. Now +that's no longer the case, you can now remove the `""` and take advantage of +writing nice `yaml` files that just work. + +```diff + spec: + steps: + input: + allowedHosts: ['github.com'] +- description: 'This is {{ parameters.name }}' ++ description: This is ${{ parameters.name }} +- repoUrl: '{{ parameters.repoUrl }}' ++ repoUrl: ${{ parameters.repoUrl }} +``` + +## No more `eq` or `not` helpers + +These helpers are no longer needed with the more expressive `api` that +`nunjucks` provides. You can simply use the built-in `nunjucks` and `jinja2` +style operators. + +```diff + spec: + steps: + input: +- if: '{{ eq parameters.value "backstage" }}' ++ if: ${{ parameters.value === "backstage" }} + ... +``` + +And then for the `not` + +```diff + spec: + steps: + input: +- if: '{{ not parameters.value "backstage" }}' ++ if: ${{ parameters.value !== "backstage" }} + ... +``` + +Much better right? ✨ + +## No more `json` helper + +This helper is no longer needed, as we've added support for complex values and +supporting the additional primitive values now rather than everything being a +`string`. This means that now that you can pass around `parameters` and it +should all work as expected and keep the type that has been declared in the +input schema. + +```diff + spec: + parameters: + test: + type: number + name: Test Number + address: + type: object + required: + - line1 + properties: + line1:πŸ™ + type: string + name: Line 1 + line2: + type: string + name: Line 2 + + steps: + - id: test step + action: run:something + input: +- address: '{{ json parameters.address }}' ++ address: ${{ parameters.address }} +- number: '{{ parameters.number }}' ++ number: ${{ parameters.number }} # this will now make sure that the type of number is a number πŸ™ +``` + +## `parseRepoUrl` is now a `filter` + +All calls to `parseRepoUrl` are now a `jinja2` `filter`, which means you'll need +to update the syntax. + +```diff + spec: + steps: + input: +- repoUrl: '{{ parseRepoUrl parameters.repoUrl }}' ++ repoUrl: ${{ parameters.repoUrl | parseRepoUrl }} + ... +``` + +Now we have complex value support here too, expect that this `filter` will go +away in future versions and the `RepoUrlPicker` will return an object so +`parameters.repoUrl` will already be a +`{ host: string; owner: string; repo: string }` πŸš€ + +### Summary + +Of course, we're always available on [discord](https://discord.gg/MUpMjP2) if +you're stuck or something's not working as expected. You can also +[raise an issue](https://github.com/backstage/backstage/issues/new/choose) with +feedback or bugs! diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 46cbc2a8a2..fbe9f36540 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -63,6 +63,10 @@ techdocs: # (Required) Cloud Storage Bucket Name bucketName: 'techdocs-storage' + # (Optional) Location in storage bucket to save files + # If not set, the default location will be the root of the storage bucket + bucketRootPath: '/' + # (Optional) An API key is required to write to a storage bucket. # If missing, GOOGLE_APPLICATION_CREDENTIALS environment variable will be used. # https://cloud.google.com/docs/authentication/production @@ -75,6 +79,10 @@ techdocs: # (Required) AWS S3 Bucket Name bucketName: 'techdocs-storage' + # (Optional) Location in storage bucket to save files + # If not set, the default location will be the root of the storage bucket + bucketRootPath: '/' + # (Optional) An API key is required to write to a storage bucket. # If not set, environment variables or aws config file will be used to authenticate. # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 93ce3ad4d9..ec27a81653 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -5,11 +5,12 @@ sidebar_label: Creating and Publishing Documentation description: Guidance on how to create and publish documentation --- -This section will guide you through: +This section will guide you through how to: - [Create a basic documentation setup](#create-a-basic-documentation-setup) + - [Use any software template](#use-any-software-template) - [Use the documentation template](#use-the-documentation-template) - - [Manually add documentation setup to already existing repository](#manually-add-documentation-setup-to-already-existing-repository) + - [Enable documentation for an already existing entity](#enable-documentation-for-an-already-existing-entity) - [Writing and previewing your documentation](#writing-and-previewing-your-documentation) ## Prerequisites @@ -21,29 +22,47 @@ This section will guide you through: If you have an existing repository that you'd like to add documentation to, skip to the -[Manually add documentation setup](#manually-add-documentation-setup-to-already-existing-repository) -section below. Otherwise, continue reading to start a documentation repo from -scratch. +[Enable documentation for an already existing entity setup](#enable-documentation-for-an-already-existing-entity) +section below. Otherwise, continue reading to create a new software entity +including documentation from scratch. + +### Use any software template + +TechDocs is built on top of the +[docs like code approach](https://www.docslikecode.com/about/). This, in short, +means that you should keep documentation close to the code. + +Your Backstage app has a set of software templates added by default. All of +these software templates include everything you need to get your TechDocs site +up and running and to start writing your documentation. + +If you have created software templates that do not include documentation by +default, we highly recommend you to set that up. Follow our how-to guide +[How to add documentation setup to your software templates](./how-to-guides.md#how-to-add-the-documentation-setup-to-your-software-templates) +to get started. ### Use the documentation template -Your working Backstage instance should by default have a documentation template -added. If not, copy the catalog locations from the +There could be _some_ situations where you don't want to keep your docs close to +your code, but still want to publish documentation - for example, an onboarding +tutorial. For this use case, we have put together a documentation template. Your +Backstage instance should by default have a documentation template added. If +not, copy the catalog locations from the [create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs) -to add the documentation template. The template creates a component with only -TechDocs configuration and default markdown files as below mentioned in manual -documentation setup, and is otherwise empty. +to add the documentation template. The template creates a component with +**only** TechDocs configuration and default markdown files, and is otherwise +empty. ![Documentation Template](../../assets/techdocs/documentation-template.png) Create an entity from the documentation template and you will get the needed setup for free. -### Manually add documentation setup to already existing repository +### Enable documentation for an already existing entity Prerequisites: -- An existing component +- An existing entity [registered in backstage](../software-catalog/index.md#adding-components-to-the-catalog) (e.g. via a `catalog-info.yaml` file). @@ -72,11 +91,11 @@ metadata: The [`backstage.io/techdocs-ref` annotation](../software-catalog/well-known-annotations.md#backstageiotechdocs-ref) is used by TechDocs to download the documentation source files for generating an -Entity's TechDocs site. +entity's TechDocs site. -Create a `/docs` folder in the root of the project with at least an `index.md` -file. _(If you add more markdown files, make sure to update the nav in the -mkdocs.yml file to get a proper navigation for your documentation.)_ +Create a `/docs` folder in the root of your repository with at least an +`index.md` file in it. _(If you add more markdown files, make sure to update the +nav in the mkdocs.yml file to get a proper navigation for your documentation.)_ > Note - Although `docs` is a popular directory name for storing documentation, > it can be renamed to something else and can be configured by `mkdocs.yml`. See @@ -95,9 +114,10 @@ updated documentation next time you run Backstage! ## Writing and previewing your documentation -Using the `techdocs-cli` you can preview your docs inside a local Backstage -instance and get live reload on changes. This is useful when you want to preview -your documentation while writing. +Using the [techdocs-cli](https://github.com/backstage/techdocs-cli) you can +preview your docs inside a local Backstage instance and get live reload on +changes. This is useful when you want to preview your documentation while +writing. To do this you can run: diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 0b1809dc22..d79f6bb891 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -215,10 +215,10 @@ techdocs: publisher: type: 'local' generator: - techdocs: local + runIn: local ``` -Setting `generators.techdocs` to `local` means you will have to make sure your +Setting `generator.runIn` to `local` means you will have to make sure your environment is compatible with techdocs. You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 9c41c6dde2..8ff486af37 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -110,7 +110,8 @@ TechDocs uses a composability pattern similar to the Search and Catalog plugins in Backstage. While a default table experience, similar to the one provided by the Catalog plugin, is made available for ease-of-use, it's possible for you to provide a completely custom experience, tailored to the needs of your -organization. +organization. For example, TechDocs comes with an alternative grid based layout +(``). This is done in your `app` package. By default, you might see something like this in your `App.tsx`: @@ -348,3 +349,75 @@ const app = createApp({ ], }); ``` + +## How to add the documentation setup to your software templates + +[Software Templates](https://backstage.io/docs/features/software-templates/software-templates-index) +in Backstage is a tool that can help your users to create new components out of +already configured templates. It comes with a set of default templates to use, +but you can also +[add your own templates](https://backstage.io/docs/features/software-templates/adding-templates). + +If you have your own templates set up, we highly recommend that you include the +required setup for TechDocs in those templates. When creating a new component, +your users will then get a TechDocs site up and running automatically, ready for +them to start writing technical documentation. + +The purpose of this how-to guide is to walk you through how to add the required +configuration and some default markdown files to your new template. You can use +the +[react-ssr-template](https://github.com/backstage/software-templates/tree/main/scaffolder-templates/react-ssr-template) +as a reference when walking through the steps. + +Prerequisites: + +- An existing software template including a `template.yaml` together with a + skeleton folder including at least a `catalog-info.yaml`. + +1. Update your component's entity description by adding the following lines to + the `catalog-info.yaml` in your skeleton folder. + +```yaml +annotations: + backstage.io/techdocs-ref: dir:. +``` + +The +[`backstage.io/techdocs-ref` annotation](../software-catalog/well-known-annotations.md#backstageiotechdocs-ref) +is used by TechDocs to download the documentation source files for generating an +entity's TechDocs site. + +2. Create an `mkdocs.yml` file in the root of your skeleton folder with the + following content: + +```yaml +site_name: ${{values.component_id}} +site_description: ${{values.description}} + +nav: + - Introduction: index.md + +plugins: + - techdocs-core +``` + +3. Create a `/docs` folder in the skeleton folder with at least an `index.md` + file in it. + +The `docs/index.md` can for example have the following content: + +```md +# ${{ values.component_id }} + +${{ values.description }} + +## Getting started + +Start writing your documentation by adding more markdown (.md) files to this +folder (/docs) or replace the content in this file. +``` + +> Note: The values of `site_name`, `component_id` and `site_description` depends +> on how you have configured your `template.yaml` + +Done! You now have support for TechDocs in your own software template! diff --git a/docs/getting-started/contributors.md b/docs/getting-started/contributors.md index e12c0c5f17..28b062b5ca 100644 --- a/docs/getting-started/contributors.md +++ b/docs/getting-started/contributors.md @@ -96,7 +96,7 @@ yarn start # Start serving the example app, use --check to include type checks a yarn storybook # Start local storybook, useful for working on components in @backstage/core-components -yarn workspace @backstage/plugin-welcome start # Serve welcome plugin only, also supports --check +yarn workspace @backstage/plugin-api-docs start # Serve api-docs plugin only, also supports --check yarn tsc # Run typecheck, use --watch for watch mode yarn tsc:full # Run full type checking, for example without skipLibCheck, use in CI @@ -139,3 +139,92 @@ default app configs. You can learn more about the local configuration in [Static Configuration in Backstage](../conf/) section. + +## Writing changesets + +Changesets are an important part of the development process. They are used to +generate Changelog entries for all changes to the project. Ultimately they are +read by the end users to learn about important changes and fixes to the project. +Some of these fixes might require manual intervention from users so it's +important to write changesets that users understand and can take action on. + +Here are some important do's and don'ts when writing changesets: + +### Changeset should give a clear description to what has changed + +#### Bad + +``` +--- +'@backstage/catalog': patch +--- +Fixed table layout +``` + +#### Good + +``` +--- +'@backstage/catalog': patch +--- + +Fixed bug in EntityTable component where table layout did not readjust properly below 1080x768 pixels. +``` + +### Breaking changes not caught by the type checker should be clearly marked with bold **BREAKING** text + +#### Bad + +``` +--- +'@backstage/catalog': minor +--- + +getEntity is now a function that returns a Promise. +``` + +#### Good + +``` +--- +'@backstage/catalog': minor +--- + +**BREAKING** The getEntity function now returns a Promise and **must** be awaited from now on. +``` + +### Changes to code should include a diff of the files that need updating + +#### Bad + +``` +--- +'@backstage/catalog': patch +--- + +**BREAKING** The catalogEngine now requires a flux capacitor to be passed. +``` + +#### Good + + --- + '@backstage/catalog': patch + --- + + **BREAKING** The catalog createRouter now requires that a `FluxCapacitor` is + passed to the router. + + These changes are **required** to `packages/backend/src/plugins/catalog.ts` + + ```diff + + import { FluxCapacitor } from '@backstage/time'; + + const fluxCapacitor = new FluxCapacitor(); + return await createRouter({ + entitiesCatalog, + locationAnalyzer, + locationService, + + fluxCapacitor, + logger: env.logger, + config: env.config, + }); + ``` diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index ac78685a82..e5d15d5845 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -132,56 +132,3 @@ Now you're free to hack away on your own Backstage installation! As you get more experienced with the app, in future you can run just the frontend with `yarn start` in one window, and the backend with `yarn start-backend` in a different window. - -## Linking in local Backstage packages - -It can often be useful to try out changes to the packages in the main Backstage -repo within your own app. For example if you want to make modifications to -`@backstage/core-plugin-api` and try them out in your app. - -To link in external packages, add them to your `package.json` and `lerna.json` -workspace paths. These can be either relative or absolute paths with or without -globs. For example: - -```json -"packages": [ - "packages/*", - "plugins/*", - "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api -], -``` - -Then reinstall packages to make yarn set up symlinks: - -```bash -yarn install -``` - -With this in place you can now modify the `@backstage/core-plugin-api` package -within the main repo, and have those changes be reflected and tested in your -app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as -normal. - -Note that for backend packages you need to make sure that linked packages are -not dependencies of any non-linked package. If you for example want to work on -`@backstage/backend-common`, you need to also link in other backend plugins and -packages that depend on `@backstage/backend-common`, or temporarily disable -those plugins in your backend. This is because the transformation of backend -module tree stops whenever a non-local package is encountered, and from that -point node will `require` packages directly for that entire module subtree. - -Type checking can also have issues when linking in external packages, since the -linked in packages will use the types in the external project and dependency -version mismatches between the two projects may cause errors. To fix any of -those errors you need to sync versions of the dependencies in the two projects. -A simple way to do this can be to copy over `yarn.lock` from the external -project and run `yarn install`, although this is quite intrusive and can cause -other issues in existing projects, so use this method with care. It can often be -best to simply ignore the type errors, as app serving will work just fine -anyway. - -Another issue with type checking is that the incremental type cache doesn't -invalidate correctly for the linked in packages, causing type checking to not -reflect changes made to types. You can work around this by either setting -`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the -types cache folder `dist-types` before running `yarn tsc`. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index b01afac5c3..9632be71bf 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -4,49 +4,99 @@ title: Getting Started description: Documentation on How to get started with Backstage --- -There are two different ways to get started with Backstage: +For most Backstage installations, installing the standalone app will bring you +the best and most streamlined experience. In this guide you will: -- **Recommended:** Create a standalone app -- **Contributors:** Clone the Backstage repository +- Deploy Backstage Standalone with npm packages +- Run Backstage Standalone with a SQLite in-memory database and demo content -Creating a standalone app makes it simpler to customize the application for your -needs and stay up to date with the project. You will depend on `@backstage` -packages from npm, making your app much smaller. This is the recommended -approach for most installations. +This guide assumes a basic understanding of working on a Linux based operating +system using tools like apt-get, npm, yarn, curl. Docker knowledge is also +helpful for making the best use of your Backstage installation. -If you want to contribute plugins or to the project in general, it's easier to -fork and clone the repository. The `@backstage` packages will be included in the -clone. That will let you stay up to date with the latest changes, and give you -an easier path to make Pull Requests. +If you are planning to contribute plugins or the project in general, we advise +you to use the +[Getting Started for Contributors](https://backstage.io/docs/getting-started/running-backstage-locally) +guide to do a repository-based installation. + +### Prerequisites + +- Access to a Linux-based operating system, such as Linux, MacOS or + [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) +- An account with elevated rights +- `curl` or `wget` installed +- Node.js Active LTS Release installed (currently v14) using one of these + methods: + - Using `nvm` (recommended) + - [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script) + - [Install and change Node version with nvm](https://nodejs.org/en/download/package-manager/#nvm) + - [Binary Download](https://nodejs.org/en/download/) + - [Package manager](https://nodejs.org/en/download/package-manager/) + - [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md) +- `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install) +- `docker` [installation](https://docs.docker.com/engine/install/) +- `git` [installation](https://github.com/git-guides/install-git) +- If the system is not directly accessible over your network, the following + ports need to be opened: 3000, 7000 ### Create your Backstage App -Backstage provides the `@backstage/create-app` package to scaffold standalone -instances of Backstage. You will need to have -[Node.js](https://nodejs.org/en/download/) Active LTS Release installed -(currently v14) and [Yarn](https://classic.yarnpkg.com/en/docs/install). You -will also need to have [Docker](https://docs.docker.com/engine/install/) -installed to use some features like Software Templates and TechDocs. - -Using `npx` you can then run the following to create an app in a chosen -subdirectory of your current working directory: +To install the Backstage Standalone app, we make use of `npx`, a tool to run +Node executables straight from the registry. Running the command below will +install Backstage. The wizard will create a subdirectory inside your current +working directory. ```bash npx @backstage/create-app ``` -You will be taken through a wizard to create your app. You can read more about -this process in [Create an app](./create-an-app.md). +The wizard will ask you -### Contributing to Backstage +- The name of the app, which will also be the name of the directory +- The database type to use for the backend. For this guide, you'll be using the + SQLite option. -If you intend to make changes to the core project's packages, certain plugins, -or project documentation, then you can fork and clone -[https://github.com/backstage/backstage](https://github.com/backstage/backstage). +

+ Screenshot of the wizard asking for a name for the app, and a selection menu for the database. +

-This will let you run the latest code off of the `master` branch, fix bugs or -contribute new features, run test suites, etc. +### Run the Backstage app -You can read more in our -[CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) -guide, which can help you get setup with a Backstage development environment. +When the installation is complete you can go to the application directory and +start the app. The `yarn dev` command will run both the frontend and backend as +separate processes (named `[0]` and `[1]`) in the same window. + +```bash +cd my-backstage-app +yarn dev +``` + +

+ Screenshot of the command output, with the message web pack compiled successfully. +

+ +It might take a little while, but as soon as the message +`[0] webpack compiled successfully` appears, you can open a browser and directly +navigate to your freshly installed Backstage portal at `http://localhost:3000`. +You can start exploring the demo immediately. + +

+ Screenshot of the Backstage portal. +

+ +Congratulations! That should be it. Let us know how it went: +[on discord](https://discord.gg/EBHEGzX), file issues for any +[feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) +or +[plugin suggestions](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME), +or +[bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) +you have, and feel free to +[contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! + +The most common next steps are to configure Backstage, add a plugin and moving +to a more persistent database: + +- [Setting up Authentication](https://backstage.io/docs/auth/) +- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres) +- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins) diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index c359960f4d..8e17bb2cc1 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -7,7 +7,7 @@ description: Importing users and groups from a Microsoft Azure Active Directory --- The Backstage catalog can be set up to ingest organizational data - users and -teams - directly from an tenant in Microsoft Azure Active Directory via the +teams - directly from a tenant in Microsoft Azure Active Directory via the Microsoft Graph API. More details on this are available in the diff --git a/docs/integrations/google-analytics/installation.md b/docs/integrations/google-analytics/installation.md deleted file mode 100644 index d6c23cc4b0..0000000000 --- a/docs/integrations/google-analytics/installation.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -id: installation -title: Google Analytics Installation -sidebar_label: Installation -# prettier-ignore -description: Adding Google Analytics to Your App ---- - -There is a basic -[Google Analytics](https://marketingplatform.google.com/about/analytics/) -integration built into Backstage. You can enable it by adding the following to -your app configuration: - -```yaml -app: - googleAnalyticsTrackingId: UA-000000-0 -``` - -Replace the tracking ID with the one generated for you after signing up for the -Google Analytics service. - -The default behavior is only to send a pageview hit. To record more, review the -[Google Analytics developer documentation](https://developers.google.com/analytics/devguides/collection/gtagjs). diff --git a/docs/cli/commands.md b/docs/local-dev/cli-commands.md similarity index 99% rename from docs/cli/commands.md rename to docs/local-dev/cli-commands.md index 5727833562..0397ffa09c 100644 --- a/docs/cli/commands.md +++ b/docs/local-dev/cli-commands.md @@ -1,6 +1,6 @@ --- -id: commands -title: Commands +id: cli-commands +title: CLI Commands description: Descriptions of all commands available in the CLI. --- diff --git a/docs/cli/index.md b/docs/local-dev/cli-overview.md similarity index 97% rename from docs/cli/index.md rename to docs/local-dev/cli-overview.md index 1b2e01be62..de9e378632 100644 --- a/docs/cli/index.md +++ b/docs/local-dev/cli-overview.md @@ -1,6 +1,6 @@ --- -id: index -title: Overview +id: cli-overview +title: CLI Overview description: Overview of the Backstage CLI --- @@ -20,7 +20,7 @@ Under the hood the CLI uses [Webpack](https://webpack.js.org/) for bundling, 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. +For a full list of CLI commands, see the [commands](./cli-commands.md) page. ## Introduction diff --git a/docs/local-dev/linking-local-packages.md b/docs/local-dev/linking-local-packages.md new file mode 100644 index 0000000000..40b722e302 --- /dev/null +++ b/docs/local-dev/linking-local-packages.md @@ -0,0 +1,56 @@ +--- +id: linking-local-packages +title: Linking in Local Packages +description: How to link in other local packages into your Backstage monorepo +--- + +It can often be useful to try out changes to the packages in the main Backstage +repo within your own app. For example if you want to make modifications to +`@backstage/core-plugin-api` and try them out in your app. + +To link in external packages, add them to your `package.json` and `lerna.json` +workspace paths. These can be either relative or absolute paths with or without +globs. For example: + +```json +"packages": [ + "packages/*", + "plugins/*", + "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api +], +``` + +Then reinstall packages to make yarn set up symlinks: + +```bash +yarn install +``` + +With this in place you can now modify the `@backstage/core-plugin-api` package +within the main repo, and have those changes be reflected and tested in your +app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as +normal. + +Note that for backend packages you need to make sure that linked packages are +not dependencies of any non-linked package. If you for example want to work on +`@backstage/backend-common`, you need to also link in other backend plugins and +packages that depend on `@backstage/backend-common`, or temporarily disable +those plugins in your backend. This is because the transformation of backend +module tree stops whenever a non-local package is encountered, and from that +point node will `require` packages directly for that entire module subtree. + +Type checking can also have issues when linking in external packages, since the +linked in packages will use the types in the external project and dependency +version mismatches between the two projects may cause errors. To fix any of +those errors you need to sync versions of the dependencies in the two projects. +A simple way to do this can be to copy over `yarn.lock` from the external +project and run `yarn install`, although this is quite intrusive and can cause +other issues in existing projects, so use this method with care. It can often be +best to simply ignore the type errors, as app serving will work just fine +anyway. + +Another issue with type checking is that the incremental type cache doesn't +invalidate correctly for the linked in packages, causing type checking to not +reflect changes made to types. You can work around this by either setting +`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the +types cache folder `dist-types` before running `yarn tsc`. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md new file mode 100644 index 0000000000..7adf2650df --- /dev/null +++ b/docs/plugins/analytics.md @@ -0,0 +1,298 @@ +--- +id: analytics +title: Plugin Analytics +description: Measuring usage of your Backstage instance. +--- + +Setting up, maintaining, and iterating on an instance of Backstage can be a +large investment. To help measure return on this investment, Backstage comes +with an event-based Analytics API that grants app integrators the flexibility to +collect and analyze Backstage usage in the analytics tool of their choice, while +providing plugin developers a standard interface for instrumenting key user +interactions. + +## Concepts + +- **Events** consist of, at a minimum, an `action` (like `click`) and a + `subject` (like `thing that was clicked on`). +- **Attributes** represent additional dimensional data (in the form of key/value + pairs) that may be provided on an event-by-event basis. To continue the above + example, the URL a user clicked to might look like `{ "to": "/a/page" }`. +- **Context** represents the broader context in which an event took place. By + default, information like `pluginId`, `extension`, and `routeRef` are + provided. + +This composition of events aims to allow analysis at different levels of detail, +enabling very granular questions (like "what is the most clicked on thing on a +particular route") as well as very high-level questions (like "what is the most +used plugin in my Backstage instance") to be answered. + +## Supported Analytics Tools + +While all that's needed to consume and forward these events to an analytics tool +is a concrete implementation of [AnalyticsApi][analytics-api-type], common +integrations are packaged and provided as plugins. Find your analytics tool of +choice below. + +| Analytics Tool | Support Status | +| ---------------------- | -------------- | +| [Google Analytics][ga] | Yes βœ… | + +To suggest an integration, please [open an issue][add-tool] for the analytics +tool your organization uses. Or jump to [Writing Integrations][int-howto] to +learn how to contribute the integration yourself! + +[ga]: + https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md +[add-tool]: + https://github.com/backstage/backstage/issues/new?assignees=&labels=plugin&template=plugin_template.md&title=%5BAnalytics+Module%5D+THE+ANALYTICS+TOOL+TO+INTEGRATE +[int-howto]: #writing-integrations +[analytics-api-type]: + https://backstage.io/docs/reference/core-plugin-api.analyticsapi + +## Key Events + +The following table summarizes events that, depending on the plugins you have +installed, may be captured. + +| Action | Provided By | Subject | +| ---------- | -------------- | ----------------------------------------- | +| `navigate` | Backstage Core | The URL of the page that was navigated to | +| `click` | Backstage Core | The text of the link that was clicked on | + +If there is an event you'd like to see captured, please [open an +issue][add-event] describing the event you want to see and the questions it +would help you answer. Or jump to [Capturing Events][event-howto] to learn how +to contribute the instrumentation yourself! + +_OSS plugin maintainers: feel free to document your events in the table above._ + +[add-event]: +https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md&title=[Analytics%20Event]:%20THE+EVENT+TO+CAPTURE +[event-howto]: #capturing-events + +## Writing Integrations + +Analytics event forwarding is implemented as a Backstage utility API. Just as +you might provide a custom API implementation for errors or SCM Authentication, +you can provide one for analytics. + +The provided API need only provide a single method `captureEvent`, which takes +an `AnalyticsEvent` object. + +```ts +import { + analyticsApiRef, + AnalyticsEvent, + AnyApiFactory, + createApiFactory, +} from '@backstage/core-plugin-api'; + +export const apis: AnyApiFactory[] = [ + createApiFactory(analyticsApiRef, { + captureEvent: (event: AnalyticsEvent) => { + window._AcmeAnalyticsQ.push(event); + }, + }), +]; +``` + +In reality, you would likely want to encapsulate instantiation logic and pull +some details from configuration. A more complete example might look like: + +```ts +import { + AnalyticsApi, + analyticsApiRef, + AnalyticsEvent, + AnyApiFactory, + configApiRef, + createApiFactory, +} from '@backstage/core-plugin-api'; +import { AcmeAnalytics } from 'acme-analytics'; + +class AcmeAnalytics implements AnalyticsApi { + private constructor(accountId: number) { + AcmeAnalytics.init(accountId); + } + + static fromConfig(config) { + const accountId = config.getString('app.analytics.acme.id'); + return new AcmeAnalytics(accountId); + } + + captureEvent(event: AnalyticsEvent) { + const { action, ...rest } = event; + AcmeAnalytics.send(action, rest); + } +} + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), + }), +]; +``` + +If you are integrating with an analytics service (as opposed to an internal +tool), consider contributing your API implementation as a plugin! + +By convention, such packages should be named +`@backstage/analytics-module-[name]`, and any configuration should be keyed +under `app.analytics.[name]`. + +## Capturing Events + +To instrument an event in a component, start by retrieving an analytics tracker +using the `useAnalytics()` hook provided by `@backstage/core-plugin-api`. The +tracker includes a `captureEvent` method which takes an `action` and a `subject` +as arguments. + +```ts +import { useAnalytics } from '@backstage/core-plugin-api'; + +const analytics = useAnalytics(); +analytics.captureEvent('deploy', serviceName); +``` + +### Providing Extra Attributes + +Additional dimensional `attributes` as well as a numeric `value` can be provided +on a third `options` argument if/when relevant for the event: + +```ts +analytics.captureEvent('merge', pullRequestName, { + value: pullRequestAgeInMinutes, + attributes: { + org, + repo, + }, +}); +``` + +In the above example, an event resembling the following object would be +captured: + +```json +{ + "action": "merge", + "subject": "Name of Pull Request", + "value": 60, + "attributes": { + "org": "some-org", + "repo": "some-repo" + } +} +``` + +### Providing Context for Events + +The `attributes` option is good for capturing details available to you within +the component that you're instrumenting. For capturing metadata only available +further up the react tree, or to help app integrators aggregate distinct events +by some common value, use an ``. + +```tsx +import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; + +const MyComponent = ({ value }) => { + const analytics = useAnalytics(); + const handleClick = () => analytics.captureEvent('check', value); + return ; +}; + +const MyWrapper = () => { + return ( + + + + ); +}; +``` + +In the above example, clicking on `` would result in an analytics +event resembling: + +```json +{ + "action": "check", + "subject": "Some Value", + "context": { + "segment": "xyz" + } +} +``` + +Note that, for brevity in the example above, the context keys provided by +Backstage core (`pluginId`, `extension`, and `routeRef`) have been omitted. In +reality, those details would be included alongside any additional context +provided by you. + +Analytics contexts can be nested; their values are merged down the react tree, +allowing keys to be overwritten. + +### Event Naming Considerations + +An event is split into its constituent parts to enable analysis at various +levels of granularity. In order to maintain this flexibility at analysis-time, +it's important to keep each of these levels of detail disaggregated. + +- Avoid providing an overly specific `action`. For example, instead of + `filterEntityTable`, consider just using `filter` as the action, and allowing + `EntityTable` to be specified as part of the event's `context` (most likely + automatically as part of the `extension` in which the `filter` event was + captured). + +- On the flip side, when adding `attributes` to an event, look at existing + events and see if the data you are capturing matches the intention, type, or + even the content of _their_ `attributes`. For instance, it may be common for + events that involve the Catalog to add details like entity `name`, `kind`, + and/or `namespace` as `attributes`. Using the same keys in your event will + ensure that events instrumented across plugins can easily be aggregated. + +### Unit Testing Event Capture + +The `@backstage/test-utils` package includes a `MockAnalyticsApi` implementation +that you can use in your unit tests to spy on and make assertions about any +analytics events captured. + +Use it like this: + +```tsx +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { MockAnalyticsApi, wrapInTestApp } from '@backstage/test-utils'; +import { render, fireEvent, waitFor } from '@testing-library/react'; + +describe('SomeComponent', () => { + it('should capture event on click', () => { + // Use the Mock Analytics API to spy on event captures. + const apiSpy = new MockAnalyticsApi(); + + // Render the component being tested + const { getByText } = render( + wrapInTestApp( + + + , + ), + ); + + // Fire the event that triggers event capture. + fireEvent.click(getByText('some component text')); + + // Assert that the event was captured with the expected data. + await waitFor(() => { + expect(apiSpy.getEvents()[0]).toMatchObject({ + action: 'expected action', + subject: 'expected subject', + attributes: { + foo: 'bar', + }, + }); + }); + }); +}); +``` diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index c35f4b91fa..ef415d107a 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -10,7 +10,8 @@ A Backstage Plugin adds functionality to Backstage. To create a new plugin, make sure you've run `yarn install` and installed dependencies, then run the following on your command line (a shortcut to -invoking the [`backstage-cli create-plugin`](../cli/commands.md#create-plugin)) +invoking the +[`backstage-cli create-plugin`](../local-dev/cli-commands.md#create-plugin)) from the root of your project. ```bash diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index f9f7590e09..1be1502b6b 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -36,7 +36,7 @@ that we provide. This gives us a way to automate some of the work required to create a GitHub app. You can read more about the -[`backstage-cli create-github-app` method](../cli/commands.md#create-github-app). +[`backstage-cli create-github-app` method](../local-dev/cli-commands.md#create-github-app). Once you've gone through the CLI command, it should produce a YAML file in the root of the project which you can then use as an `include` in your @@ -60,7 +60,7 @@ The YAML file must include the following information. Please note that the indentation for the `privateKey` is required. ```yaml -appId: 1 +appId: app id clientId: client id clientSecret: client secret webhookSecret: webhook secret @@ -95,7 +95,7 @@ If you want to limit the GitHub app installations visible to backstage you may optionally include the `allowedInstallationOwners` option. ```yaml -appId: 1 +appId: app id allowedInstallationOwners: ['GlobexCorp'] clientId: client id clientSecret: client secret diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index b5a6a0ba60..d8778f07f8 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -8,11 +8,6 @@ description: Adding Observability to Your Plugin This article briefly describes the observability options that are available to a Backstage integrator. -## Google Analytics - -See how to install Google Analytics in your app -[here](../integrations/google-analytics/installation.md) - ## Datadog RUM Events See how to install Datadog Events in your app diff --git a/microsite/data/plugins/backstage-analytics-module-ga.yaml b/microsite/data/plugins/backstage-analytics-module-ga.yaml new file mode 100644 index 0000000000..0a03ec20e8 --- /dev/null +++ b/microsite/data/plugins/backstage-analytics-module-ga.yaml @@ -0,0 +1,9 @@ +--- +title: 'Analytics Module: Google Analytics' +author: Spotify +authorUrl: https://github.com/spotify +category: Analytics +description: Track usage of your Backstage instance using Google Analytics. +documentation: https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md +iconUrl: img/ga-icon.png +npmPackageName: '@backstage/plugin-analytics-module-ga' diff --git a/microsite/data/plugins/gke-usage.yaml b/microsite/data/plugins/gke-usage.yaml index 2ca9f5784a..e092f84973 100644 --- a/microsite/data/plugins/gke-usage.yaml +++ b/microsite/data/plugins/gke-usage.yaml @@ -1,7 +1,7 @@ --- title: GKE Usage author: BESTSELLER -authorUrl: bestsellerit.com +authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you the cost and resource usage of your application within Google Kubernetes Engine (GKE). documentation: https://github.com/BESTSELLER/backstage-plugin-gkeusage/blob/master/README.md diff --git a/microsite/data/plugins/grafana.yaml b/microsite/data/plugins/grafana.yaml new file mode 100644 index 0000000000..ff4e33e3df --- /dev/null +++ b/microsite/data/plugins/grafana.yaml @@ -0,0 +1,13 @@ +--- +title: Grafana +author: K-Phoen +authorUrl: https://github.com/K-Phoen +category: Monitoring +description: Associate alerts and dashboards to components. +documentation: https://github.com/K-Phoen/backstage-plugin-grafana/ +iconUrl: https://avatars.githubusercontent.com/u/7195757?s=200&v=4 +npmPackageName: '@k-phoen/backstage-plugin-grafana' +tags: + - dashboards + - monitoring + - alerting diff --git a/microsite/data/plugins/harbor.yaml b/microsite/data/plugins/harbor.yaml index 4e31113c83..9023e0c434 100644 --- a/microsite/data/plugins/harbor.yaml +++ b/microsite/data/plugins/harbor.yaml @@ -1,7 +1,7 @@ --- title: Harbor author: BESTSELLER -authorUrl: bestsellerit.com +authorUrl: https://bestsellerit.com category: Discovery description: This plugin will show you information about Docker images within the Harbor cloud native registry. documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master/README.md diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 9cf3b204c2..bee9cd7180 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -29,7 +29,14 @@ "getting-started/contributors", "getting-started/project-structure" ], - "CLI": ["cli/index", "cli/commands"], + "Local Development": [ + { + "type": "subcategory", + "label": "CLI", + "ids": ["local-dev/cli-overview", "local-dev/cli-commands"] + }, + "local-dev/linking-local-packages" + ], "Core Features": [ { "type": "subcategory", @@ -71,8 +78,7 @@ "features/software-templates/builtin-actions", "features/software-templates/writing-custom-actions", "features/software-templates/writing-custom-field-extensions", - "features/software-templates/template-legacy", - "features/software-templates/migrating-from-v1alpha1-to-v1beta2" + "features/software-templates/migrating-from-v1beta2-to-v1beta3" ] }, { @@ -83,7 +89,8 @@ "features/search/getting-started", "features/search/concepts", "features/search/architecture", - "features/search/search-engines" + "features/search/search-engines", + "features/search/how-to-guides" ] }, { @@ -146,11 +153,6 @@ "label": "GitLab", "ids": ["integrations/gitlab/locations"] }, - { - "type": "subcategory", - "label": "Google Analytics", - "ids": ["integrations/google-analytics/installation"] - }, { "type": "subcategory", "label": "Google GCS", @@ -170,6 +172,7 @@ "plugins/structure-of-a-plugin", "plugins/integrating-plugin-into-software-catalog", "plugins/composability", + "plugins/analytics", { "type": "subcategory", "label": "Backends and APIs", @@ -210,6 +213,8 @@ "label": "Included providers", "ids": [ "auth/auth0/provider", + "auth/atlassian/provider", + "auth/bitbucket/provider", "auth/microsoft/provider", "auth/github/provider", "auth/gitlab/provider", diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index ac432372b1..139fa7b1d2 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1217,3 +1217,7 @@ code { .medium-zoom-image { z-index: 10000; } + +h3.collapsible span.arrow { + margin-right: 4px; +} diff --git a/microsite/static/img/ga-icon.png b/microsite/static/img/ga-icon.png new file mode 100644 index 0000000000..8eaebd06aa Binary files /dev/null and b/microsite/static/img/ga-icon.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 366411c7c4..2f4aa4203f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,9 +27,11 @@ nav: - Key Concepts: 'getting-started/concepts.md' - Contributors: 'getting-started/contributors.md' - Project Structure: 'getting-started/project-structure.md' - - CLI: - - Overview: 'cli/index.md' - - Commands: 'cli/commands.md' + - Local Development: + - CLI: + - Overview: 'local-dev/cli-overview.md' + - Commands: 'local-dev/cli-commands.md' + - Linking in Local Packages: 'local-dev/linking-local-packages.md' - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' @@ -66,6 +68,7 @@ nav: - Concepts: 'features/search/concepts.md' - Search Architecture: 'features/search/architecture.md' - Search Engines: 'features/search/search-engines.md' + - HOW TO guides: 'features/search/how-to-guides.md' - TechDocs: - Overview: 'features/techdocs/README.md' - Getting Started: 'features/techdocs/getting-started.md' @@ -97,8 +100,6 @@ nav: - Org Data: 'integrations/github/org.md' - GitLab: - Locations: 'integrations/gitlab/locations.md' - - Google Analytics: - - Installation: 'integrations/google-analytics/installation.md' - Google GCS: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: @@ -112,6 +113,7 @@ nav: - Plugin Development: 'plugins/plugin-development.md' - Integrate into the Software Catalog: 'plugins/integrating-plugin-into-software-catalog.md' - Composability System: 'plugins/composability.md' + - Plugin Analytics: 'plugins/analytics.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' diff --git a/package.json b/package.json index 03a502fee2..4aa6905085 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "devDependencies": { "@types/webpack": "^5.28.0", "@changesets/cli": "^2.14.0", - "@octokit/openapi-types": "^2.2.0", "@spotify/prettier-config": "^11.0.0", "command-exists": "^1.2.9", "concurrently": "^6.0.0", @@ -71,7 +70,6 @@ "lerna": "^4.0.0", "lint-staged": "^11.1.2", "prettier": "^2.2.1", - "recursive-readdir": "^2.2.2", "shx": "^0.3.2", "yarn-lock-check": "^1.0.5" }, @@ -87,10 +85,5 @@ "*.md": [ "node ./scripts/check-docs-quality" ] - }, - "jest": { - "transformModules": [ - "@asyncapi/react-component" - ] } } diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 3cc1f0aea7..1dee41af56 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,67 @@ # example-app +## 0.2.51 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/plugin-techdocs@0.12.3 + - @backstage/plugin-sentry@0.3.26 + - @backstage/plugin-user-settings@0.3.10 + - @backstage/plugin-catalog-graph@0.2.1 + - @backstage/plugin-jenkins@0.5.11 + - @backstage/core-app-api@0.1.18 + - @backstage/core-plugin-api@0.1.11 + - @backstage/cli@0.8.0 + - @backstage/plugin-catalog@0.7.2 + - @backstage/plugin-catalog-import@0.7.3 + - @backstage/plugin-catalog-react@0.6.1 + - @backstage/catalog-model@0.9.5 + +## 0.2.50 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.6.0 + - @backstage/plugin-catalog@0.7.1 + - @backstage/plugin-api-docs@0.6.12 + - @backstage/plugin-scaffolder@0.11.8 + - @backstage/core-app-api@0.1.17 + - @backstage/core-components@0.7.0 + - @backstage/theme@0.2.11 + - @backstage/plugin-graphiql@0.2.20 + - @backstage/cli@0.7.16 + - @backstage/plugin-home@0.4.4 + - @backstage/plugin-catalog-graph@0.2.0 + - @backstage/plugin-search@0.4.15 + - @backstage/plugin-techdocs@0.12.2 + - @backstage/plugin-kubernetes@0.4.17 + - @backstage/plugin-azure-devops@0.1.1 + - @backstage/plugin-badges@0.2.13 + - @backstage/plugin-catalog-import@0.7.2 + - @backstage/plugin-circleci@0.2.27 + - @backstage/plugin-cloudbuild@0.2.27 + - @backstage/plugin-code-coverage@0.1.15 + - @backstage/plugin-explore@0.3.20 + - @backstage/plugin-github-actions@0.4.22 + - @backstage/plugin-jenkins@0.5.10 + - @backstage/plugin-kafka@0.2.19 + - @backstage/plugin-lighthouse@0.2.29 + - @backstage/plugin-org@0.3.27 + - @backstage/plugin-pagerduty@0.3.17 + - @backstage/plugin-rollbar@0.3.18 + - @backstage/plugin-sentry@0.3.25 + - @backstage/plugin-todo@0.1.14 + - @backstage/integration-react@0.1.12 + - @backstage/plugin-cost-insights@0.11.10 + - @backstage/plugin-gcp-projects@0.3.8 + - @backstage/plugin-newrelic@0.3.8 + - @backstage/plugin-shortcuts@0.1.12 + - @backstage/plugin-tech-radar@0.4.11 + - @backstage/plugin-user-settings@0.3.9 + ## 0.2.49 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index d1892438d8..833e146d45 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,48 +1,49 @@ { "name": "example-app", - "version": "0.2.49", + "version": "0.2.51", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/cli": "^0.7.15", - "@backstage/core-app-api": "^0.1.16", - "@backstage/core-components": "^0.6.1", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/integration-react": "^0.1.11", - "@backstage/plugin-api-docs": "^0.6.11", - "@backstage/plugin-badges": "^0.2.12", - "@backstage/plugin-catalog": "^0.7.0", - "@backstage/plugin-catalog-graph": "^0.1.3", - "@backstage/plugin-catalog-import": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.5.2", - "@backstage/plugin-circleci": "^0.2.26", - "@backstage/plugin-cloudbuild": "^0.2.26", - "@backstage/plugin-code-coverage": "^0.1.14", - "@backstage/plugin-cost-insights": "^0.11.9", - "@backstage/plugin-explore": "^0.3.19", - "@backstage/plugin-gcp-projects": "^0.3.7", - "@backstage/plugin-github-actions": "^0.4.21", - "@backstage/plugin-graphiql": "^0.2.19", - "@backstage/plugin-home": "^0.4.3", - "@backstage/plugin-jenkins": "^0.5.9", - "@backstage/plugin-kafka": "^0.2.18", - "@backstage/plugin-kubernetes": "^0.4.16", - "@backstage/plugin-lighthouse": "^0.2.28", - "@backstage/plugin-newrelic": "^0.3.7", - "@backstage/plugin-org": "^0.3.26", - "@backstage/plugin-pagerduty": "0.3.16", - "@backstage/plugin-rollbar": "^0.3.17", - "@backstage/plugin-scaffolder": "^0.11.7", - "@backstage/plugin-search": "^0.4.14", - "@backstage/plugin-sentry": "^0.3.24", - "@backstage/plugin-shortcuts": "^0.1.11", - "@backstage/plugin-tech-radar": "^0.4.10", - "@backstage/plugin-techdocs": "^0.12.1", - "@backstage/plugin-todo": "^0.1.13", - "@backstage/plugin-user-settings": "^0.3.8", + "@backstage/catalog-model": "^0.9.5", + "@backstage/cli": "^0.8.0", + "@backstage/core-app-api": "^0.1.18", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/integration-react": "^0.1.12", + "@backstage/plugin-api-docs": "^0.6.12", + "@backstage/plugin-azure-devops": "^0.1.1", + "@backstage/plugin-badges": "^0.2.13", + "@backstage/plugin-catalog": "^0.7.2", + "@backstage/plugin-catalog-graph": "^0.2.1", + "@backstage/plugin-catalog-import": "^0.7.3", + "@backstage/plugin-catalog-react": "^0.6.1", + "@backstage/plugin-circleci": "^0.2.27", + "@backstage/plugin-cloudbuild": "^0.2.27", + "@backstage/plugin-code-coverage": "^0.1.15", + "@backstage/plugin-cost-insights": "^0.11.10", + "@backstage/plugin-explore": "^0.3.20", + "@backstage/plugin-gcp-projects": "^0.3.8", + "@backstage/plugin-github-actions": "^0.4.22", + "@backstage/plugin-graphiql": "^0.2.20", + "@backstage/plugin-home": "^0.4.4", + "@backstage/plugin-jenkins": "^0.5.11", + "@backstage/plugin-kafka": "^0.2.19", + "@backstage/plugin-kubernetes": "^0.4.17", + "@backstage/plugin-lighthouse": "^0.2.29", + "@backstage/plugin-newrelic": "^0.3.8", + "@backstage/plugin-org": "^0.3.27", + "@backstage/plugin-pagerduty": "0.3.17", + "@backstage/plugin-rollbar": "^0.3.18", + "@backstage/plugin-scaffolder": "^0.11.8", + "@backstage/plugin-search": "^0.4.15", + "@backstage/plugin-sentry": "^0.3.26", + "@backstage/plugin-shortcuts": "^0.1.12", + "@backstage/plugin-tech-radar": "^0.4.11", + "@backstage/plugin-techdocs": "^0.12.3", + "@backstage/plugin-todo": "^0.1.14", + "@backstage/plugin-user-settings": "^0.3.10", "@backstage/search-common": "^0.2.0", - "@backstage/theme": "^0.2.10", + "@backstage/theme": "^0.2.11", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -62,7 +63,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.18", + "@backstage/test-utils": "^0.1.19", "@rjsf/core": "^3.0.0", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index da4d3cf3b8..d86f09f0b0 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -83,7 +83,7 @@ window, document, 'script', - 'https://www.datadoghq-browser-agent.com/datadog-rum.js', + 'https://www.datadoghq-browser-agent.com/datadog-rum-v3.js', 'DD_RUM', ); DD_RUM.onReady(function () { diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 955e477f3c..089cb9c7cf 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -14,13 +14,17 @@ * limitations under the License. */ -import React from 'react'; -import { EntityLayout } from '@backstage/plugin-catalog'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { cicdContent } from './EntityPage'; -import { githubActionsApiRef } from '@backstage/plugin-github-actions'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { EntityLayout } from '@backstage/plugin-catalog'; +import { + DefaultStarredEntitiesApi, + EntityProvider, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { githubActionsApiRef } from '@backstage/plugin-github-actions'; +import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { cicdContent } from './EntityPage'; describe('EntityPage Test', () => { const entity = { @@ -48,7 +52,10 @@ describe('EntityPage Test', () => { downloadJobLogsForWorkflowRun: jest.fn(), } as jest.Mocked; - const apis = ApiRegistry.with(githubActionsApiRef, mockedApi); + const apis = ApiRegistry.with(githubActionsApiRef, mockedApi).with( + starredEntitiesApiRef, + new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), + ); describe('cicdContent', () => { it('Should render GitHub Actions View', async () => { diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 62fdcf4d3f..3fe027ddad 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -33,6 +33,10 @@ import { EntityProvidedApisCard, EntityProvidingComponentsCard, } from '@backstage/plugin-api-docs'; +import { + EntityAzurePipelinesContent, + isAzureDevOpsAvailable, +} from '@backstage/plugin-azure-devops'; import { EntityBadgesDialog } from '@backstage/plugin-badges'; import { EntityAboutCard, @@ -183,6 +187,10 @@ export const cicdContent = ( + + + + ( -
- +
} pageTitleOverride="Home"> +
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index e7b897692e..131b160a36 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/backend-common +## 0.9.7 + +### Patch Changes + +- be59619212: Add "rate limit exceeded" to error from GithubUrlReader if that is the cause of a read failure +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/config-loader@0.7.0 + - @backstage/errors@0.1.3 + ## 0.9.6 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index e2d68055bf..2336a57ec3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -18,7 +18,7 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; import { isChildPath } from '@backstage/cli-common'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { Logger as Logger_2 } from 'winston'; import { MergeResult } from 'isomorphic-git'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 16167f7803..60824f0c40 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.9.6", + "version": "0.9.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,9 +31,10 @@ "dependencies": { "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.6.10", - "@backstage/errors": "^0.1.2", + "@backstage/config-loader": "^0.7.0", + "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", + "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -77,7 +78,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@backstage/test-utils": "^0.1.18", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 79f48a2757..8e046f7f19 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { createHash } from 'crypto'; import Keyv from 'keyv'; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 04cf1ac2bb..fa49d5f015 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,7 +18,8 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader, JsonValue } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { loadConfig } from '@backstage/config-loader'; export class ObservableConfigProxy implements Config { diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index de39279a4f..c0511c6b78 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -15,7 +15,8 @@ */ import { Knex } from 'knex'; import { omit } from 'lodash'; -import { Config, ConfigReader, JsonObject } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { createDatabaseClient, ensureDatabaseExists, diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 8ea46070eb..1c5cce3718 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Config, JsonObject } from '@backstage/config'; +import { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; diff --git a/packages/backend-common/src/database/connectors/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts index f6e42d1945..62de2e001e 100644 --- a/packages/backend-common/src/database/connectors/postgres.ts +++ b/packages/backend-common/src/database/connectors/postgres.ts @@ -17,6 +17,7 @@ import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; +import { ForwardedError } from '@backstage/errors'; import { mergeDatabaseConfig } from '../config'; import { DatabaseConnector } from '../types'; import defaultNameOverride from './defaultNameOverride'; @@ -94,8 +95,7 @@ function requirePgConnectionString() { try { return require('pg-connection-string').parse; } catch (e) { - const message = `Postgres: Install 'pg-connection-string'`; - throw new Error(`${message}\n${e.message}`); + throw new ForwardedError("Postgres: Install 'pg-connection-string'", e); } } diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 80f59fe182..301543e8c9 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConfigReader, JsonObject } from '@backstage/config'; + +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { AwsS3UrlReader } from './AwsS3UrlReader'; @@ -135,13 +137,7 @@ describe('AwsS3UrlReader', () => { 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'awsS3', - 'awsS3-mock-object.yaml', - ), + path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), ), ), ); @@ -173,7 +169,7 @@ describe('AwsS3UrlReader', () => { ), ).rejects.toThrow( Error( - `Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, + `Could not retrieve file from S3; caused by Error: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, ), ); }); @@ -187,13 +183,7 @@ describe('AwsS3UrlReader', () => { 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'awsS3', - 'awsS3-mock-object.yaml', - ), + path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), ), ), ); @@ -228,7 +218,7 @@ describe('AwsS3UrlReader', () => { ), ).rejects.toThrow( Error( - `Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, + `Could not retrieve file from S3; caused by Error: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`, ), ); }); @@ -249,13 +239,7 @@ describe('AwsS3UrlReader', () => { 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'awsS3', - 'awsS3-mock-object.yaml', - ), + path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'), ), ), ); diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts index cb03ec4f71..7fd0f6ba19 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -27,6 +27,7 @@ import { } from './types'; import getRawBody from 'raw-body'; import { AwsS3Integration, ScmIntegrations } from '@backstage/integration'; +import { ForwardedError } from '@backstage/errors'; import { ListObjectsV2Output, ObjectList } from 'aws-sdk/clients/s3'; const parseURL = ( @@ -162,7 +163,7 @@ export class AwsS3UrlReader implements UrlReader { etag: etag, }; } catch (e) { - throw new Error(`Could not retrieve file from S3: ${e.message}`); + throw new ForwardedError('Could not retrieve file from S3', e); } } @@ -203,7 +204,7 @@ export class AwsS3UrlReader implements UrlReader { return await this.deps.treeResponseFactory.fromReadableArray(responses); } catch (e) { - throw new Error(`Could not retrieve file tree from S3: ${e.message}`); + throw new ForwardedError('Could not retrieve file tree from S3', e); } } diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index e014fa2e75..59968a18e9 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -152,7 +152,7 @@ describe('AzureUrlReader', () => { describe('readTree', () => { const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), + path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); const processor = new AzureUrlReader( @@ -264,7 +264,7 @@ describe('AzureUrlReader', () => { describe('search', () => { const repoBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), + path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); const processor = new AzureUrlReader( diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index ef61452163..5bbf3725ea 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -104,20 +104,13 @@ describe('BitbucketUrlReader', () => { describe('readTree', () => { const repoBuffer = fs.readFileSync( path.resolve( - 'src', - 'reading', - '__fixtures__', - 'bitbucket-repo-with-commit-hash.tar.gz', + __dirname, + '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', ), ); const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'bitbucket-server-repo.tar.gz', - ), + path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), ); beforeEach(() => { @@ -298,10 +291,8 @@ describe('BitbucketUrlReader', () => { describe('search hosted', () => { const repoBuffer = fs.readFileSync( path.resolve( - 'src', - 'reading', - '__fixtures__', - 'bitbucket-repo-with-commit-hash.tar.gz', + __dirname, + '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', ), ); @@ -386,12 +377,7 @@ describe('BitbucketUrlReader', () => { describe('search private', () => { const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'bitbucket-server-repo.tar.gz', - ), + path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), ); beforeEach(() => { diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index f9717a862a..c76364976d 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -221,6 +221,32 @@ describe('GithubUrlReader', () => { ).rejects.toThrow(NotModifiedError); }); + it('should throw Error with ratelimit exceeded if GitHub responds with 403 and rate limit is exceeded', async () => { + expect.assertions(1); + + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/', + (_req, res, ctx) => { + return res( + ctx.status(403), + ctx.set('X-RateLimit-Remaining', '0'), + ctx.body( + '{"message": "API rate limit exceeded for xxx.xxx.xxx.xxx..."}', + ), + ); + }, + ), + ); + + await expect( + gheProcessor.readUrl( + 'https://github.com/backstage/mock/tree/blob/main', + { etag: 'foo' }, + ), + ).rejects.toThrow(/rate limit exceeded/); + }); + it('should return etag from the response', async () => { (mockCredentialsProvider.getCredentials as jest.Mock).mockResolvedValue({ headers: { @@ -254,12 +280,7 @@ describe('GithubUrlReader', () => { describe('readTree', () => { const repoBuffer = fs.readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'backstage-mock-etag123.tar.gz', - ), + path.resolve(__dirname, '__fixtures__/backstage-mock-etag123.tar.gz'), ); const reposGithubApiResponse = { @@ -540,12 +561,7 @@ describe('GithubUrlReader', () => { describe('search', () => { const repoBuffer = fs.readFileSync( - path.resolve( - 'src', - 'reading', - '__fixtures__', - 'backstage-mock-etag123.tar.gz', - ), + path.resolve(__dirname, '__fixtures__/backstage-mock-etag123.tar.gz'), ); const githubTreeContents: GhTreeResponse['tree'] = [ diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 1eed5764ef..ca16e23fec 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -126,10 +126,21 @@ export class GithubUrlReader implements UrlReader { }; } - const message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; + let message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; if (response.status === 404) { throw new NotFoundError(message); } + + // GitHub returns a 403 response with a couple of headers indicating rate + // limit status. See more in the GitHub docs: + // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting + if ( + response.status === 403 && + response.headers.get('X-RateLimit-Remaining') === '0' + ) { + message += ' (rate limit exceeded)'; + } + throw new Error(message); } diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 51b211e698..331150bde3 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -223,7 +223,7 @@ describe('GitlabUrlReader', () => { describe('readTree', () => { const archiveBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.tar.gz'), + path.resolve(__dirname, '__fixtures__/gitlab-archive.tar.gz'), ); const projectGitlabApiResponse = { @@ -495,7 +495,7 @@ describe('GitlabUrlReader', () => { describe('search', () => { const archiveBuffer = fs.readFileSync( - path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.tar.gz'), + path.resolve(__dirname, '__fixtures__/gitlab-archive.tar.gz'), ); const projectGitlabApiResponse = { diff --git a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts index d8aac4886d..31cf2956f8 100644 --- a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { ConfigReader, JsonObject } from '@backstage/config'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts index ae557be583..951df29605 100644 --- a/packages/backend-common/src/reading/integration.test.ts +++ b/packages/backend-common/src/reading/integration.test.ts @@ -15,6 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; +import { isError } from '@backstage/errors'; import { getVoidLogger } from '../logging'; import { UrlReaders } from './UrlReaders'; @@ -71,7 +72,10 @@ function withRetries(count: number, fn: () => Promise) { error = err; } } - if (!error.message.match(/rate limit|Too Many Requests/)) { + if ( + isError(error) && + !error.message.match(/rate limit|Too Many Requests/) + ) { throw error; } else { console.warn('Request was rate limited', error); diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index da3ce66ec2..b81bd995ca 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -16,6 +16,7 @@ import Docker from 'dockerode'; import fs from 'fs-extra'; +import { ForwardedError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { ContainerRunner, RunContainerOptions } from './ContainerRunner'; @@ -45,8 +46,9 @@ export class DockerContainerRunner implements ContainerRunner { try { await this.dockerClient.ping(); } catch (e) { - throw new Error( - `This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with: ${e.message}`, + throw new ForwardedError( + 'This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with', + e, ); } diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 6e10d188df..287e40a296 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-test-utils +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/cli@0.8.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 1713f05d25..2f29c2412b 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.2", - "@backstage/cli": "^0.7.11", + "@backstage/backend-common": "^0.9.7", + "@backstage/cli": "^0.8.0", "@backstage/config": "^0.1.9", "knex": "^0.95.1", "mysql2": "^2.2.5", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.8.0", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index b527a875e7..19e9bcdd67 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,19 @@ # example-backend +## 0.2.50 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.4.4 + - @backstage/integration@0.6.8 + - @backstage/plugin-scaffolder-backend@0.15.8 + - @backstage/plugin-catalog-backend@0.17.0 + - @backstage/plugin-azure-devops-backend@0.1.2 + - @backstage/plugin-code-coverage-backend@0.1.13 + - @backstage/plugin-kubernetes-backend@0.3.17 + - example-app@0.2.50 + ## 0.2.49 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 0752f0da2e..b9a28c2853 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.49", + "version": "0.2.50", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,36 +24,36 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/catalog-client": "^0.5.0", - "@backstage/catalog-model": "^0.9.4", + "@backstage/catalog-model": "^0.9.5", "@backstage/config": "^0.1.10", - "@backstage/integration": "^0.6.7", - "@backstage/plugin-app-backend": "^0.3.16", - "@backstage/plugin-auth-backend": "^0.4.3", - "@backstage/plugin-azure-devops-backend": "^0.1.1", + "@backstage/integration": "^0.6.8", + "@backstage/plugin-app-backend": "^0.3.17", + "@backstage/plugin-auth-backend": "^0.4.5", + "@backstage/plugin-azure-devops-backend": "^0.1.3", "@backstage/plugin-badges-backend": "^0.1.11", - "@backstage/plugin-catalog-backend": "^0.16.0", - "@backstage/plugin-code-coverage-backend": "^0.1.12", + "@backstage/plugin-catalog-backend": "^0.17.1", + "@backstage/plugin-code-coverage-backend": "^0.1.14", "@backstage/plugin-graphql-backend": "^0.1.9", "@backstage/plugin-jenkins-backend": "^0.1.6", - "@backstage/plugin-kubernetes-backend": "^0.3.16", + "@backstage/plugin-kubernetes-backend": "^0.3.18", "@backstage/plugin-kafka-backend": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.13", "@backstage/plugin-rollbar-backend": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.15.7", + "@backstage/plugin-scaffolder-backend": "^0.15.10", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", "@backstage/plugin-search-backend": "^0.2.6", "@backstage/plugin-search-backend-node": "^0.4.2", "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", "@backstage/plugin-search-backend-module-pg": "^0.2.1", - "@backstage/plugin-techdocs-backend": "^0.10.4", + "@backstage/plugin-techdocs-backend": "^0.10.5", "@backstage/plugin-todo-backend": "^0.1.13", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.49", + "example-app": "^0.2.51", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -65,7 +65,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f25ec5a14e..b6c148dfba 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -38,7 +38,7 @@ import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; import { metricsInit, metricsHandler } from './metrics'; import auth from './plugins/auth'; -import azureDevOps from './plugins/azuredevops'; +import azureDevOps from './plugins/azure-devops'; import catalog from './plugins/catalog'; import codeCoverage from './plugins/codecoverage'; import kubernetes from './plugins/kubernetes'; diff --git a/packages/backend/src/plugins/azuredevops.ts b/packages/backend/src/plugins/azure-devops.ts similarity index 89% rename from packages/backend/src/plugins/azuredevops.ts rename to packages/backend/src/plugins/azure-devops.ts index e11a21ba2e..4120e655ed 100644 --- a/packages/backend/src/plugins/azuredevops.ts +++ b/packages/backend/src/plugins/azure-devops.ts @@ -18,9 +18,9 @@ import { createRouter } from '@backstage/plugin-azure-devops-backend'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; -export default async function createPlugin({ +export default function createPlugin({ logger, config, }: PluginEnvironment): Promise { - return await createRouter({ logger, config }); + return createRouter({ logger, config }); } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 6a903d0df8..7beeb4f35e 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -15,6 +15,7 @@ */ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -22,6 +23,7 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); const { processingEngine, router } = await builder.build(); await processingEngine.start(); return router; diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index a19520312f..6d40d89b66 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { createRouter } from '@backstage/plugin-kubernetes-backend'; +import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, config, }: PluginEnvironment) { - return await createRouter({ logger, config }); + const { router } = await KubernetesBuilder.createBuilder({ + logger, + config, + }).build(); + return router; } diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 8da62f6115..f8dfd59e41 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.4", - "@backstage/errors": "^0.1.2", + "@backstage/catalog-model": "^0.9.5", + "@backstage/errors": "^0.1.3", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "msw": "^0.29.0" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index e7c36953db..066b01cd50 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-model +## 0.9.5 + +### Patch Changes + +- ab2df3be33: Improved documentation for exported symbols. +- Updated dependencies + - @backstage/errors@0.1.3 + ## 0.9.4 ### Patch Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 1e7eb8fd52..52f36493b5 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -3,18 +3,18 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; import * as yup from 'yup'; -// @public @deprecated (undocumented) +// @public @deprecated export const analyzeLocationSchema: yup.SchemaOf<{ location: LocationSpec; }>; -// @public (undocumented) +// @public interface ApiEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -32,7 +32,7 @@ interface ApiEntityV1alpha1 extends Entity { export { ApiEntityV1alpha1 as ApiEntity }; export { ApiEntityV1alpha1 }; -// @public (undocumented) +// @public export const apiEntityV1alpha1Validator: KindValidator; // @public @@ -58,7 +58,7 @@ export function compareEntityToRef( context?: EntityRefContext, ): boolean; -// @public (undocumented) +// @public interface ComponentEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -79,7 +79,7 @@ interface ComponentEntityV1alpha1 extends Entity { export { ComponentEntityV1alpha1 as ComponentEntity }; export { ComponentEntityV1alpha1 }; -// @public (undocumented) +// @public export const componentEntityV1alpha1Validator: KindValidator; // @public @@ -89,7 +89,7 @@ export class DefaultNamespaceEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public (undocumented) +// @public interface DomainEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -103,10 +103,10 @@ interface DomainEntityV1alpha1 extends Entity { export { DomainEntityV1alpha1 as DomainEntity }; export { DomainEntityV1alpha1 }; -// @public (undocumented) +// @public export const domainEntityV1alpha1Validator: KindValidator; -// @public (undocumented) +// @public export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; // @public @@ -181,7 +181,7 @@ export type EntityName = { name: string; }; -// @public (undocumented) +// @public export const EntityPolicies: { allOf(policies: EntityPolicy[]): EntityPolicy; oneOf(policies: EntityPolicy[]): EntityPolicy; @@ -250,7 +250,7 @@ export function getEntitySourceLocation(entity: Entity): { target: string; }; -// @public (undocumented) +// @public interface GroupEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -272,10 +272,10 @@ interface GroupEntityV1alpha1 extends Entity { export { GroupEntityV1alpha1 as GroupEntity }; export { GroupEntityV1alpha1 }; -// @public (undocumented) +// @public export const groupEntityV1alpha1Validator: KindValidator; -// @public (undocumented) +// @public export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue; }; @@ -305,16 +305,16 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// @public (undocumented) +// @public type Location_2 = { id: string; } & LocationSpec; export { Location_2 as Location }; -// @public (undocumented) +// @public export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; -// @public (undocumented) +// @public interface LocationEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -330,23 +330,23 @@ interface LocationEntityV1alpha1 extends Entity { export { LocationEntityV1alpha1 as LocationEntity }; export { LocationEntityV1alpha1 }; -// @public (undocumented) +// @public export const locationEntityV1alpha1Validator: KindValidator; -// @public @deprecated (undocumented) +// @public @deprecated export const locationSchema: yup.SchemaOf; -// @public (undocumented) +// @public export type LocationSpec = { type: string; target: string; presence?: 'optional' | 'required'; }; -// @public @deprecated (undocumented) +// @public @deprecated export const locationSpecSchema: yup.SchemaOf; -// @public (undocumented) +// @public export function makeValidator(overrides?: Partial): Validators; // @public @@ -356,7 +356,7 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public (undocumented) +// @public export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; @@ -373,13 +373,9 @@ export function parseEntityRef( defaultKind: string; defaultNamespace: string; }, -): { - kind: string; - namespace: string; - name: string; -}; +): EntityName; -// @public (undocumented) +// @public export function parseEntityRef( ref: EntityRef, context?: { @@ -391,7 +387,7 @@ export function parseEntityRef( name: string; }; -// @public (undocumented) +// @public export function parseEntityRef( ref: EntityRef, context?: { @@ -409,28 +405,28 @@ export function parseLocationReference(ref: string): { target: string; }; -// @public (undocumented) +// @public export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; -// @public (undocumented) +// @public export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; -// @public (undocumented) +// @public export const RELATION_CHILD_OF = 'childOf'; // @public export const RELATION_CONSUMES_API = 'consumesApi'; -// @public (undocumented) +// @public export const RELATION_DEPENDENCY_OF = 'dependencyOf'; // @public export const RELATION_DEPENDS_ON = 'dependsOn'; -// @public (undocumented) +// @public export const RELATION_HAS_MEMBER = 'hasMember'; -// @public (undocumented) +// @public export const RELATION_HAS_PART = 'hasPart'; // @public @@ -439,7 +435,7 @@ export const RELATION_MEMBER_OF = 'memberOf'; // @public export const RELATION_OWNED_BY = 'ownedBy'; -// @public (undocumented) +// @public export const RELATION_OWNER_OF = 'ownerOf'; // @public @@ -448,10 +444,10 @@ export const RELATION_PARENT_OF = 'parentOf'; // @public export const RELATION_PART_OF = 'partOf'; -// @public (undocumented) +// @public export const RELATION_PROVIDES_API = 'providesApi'; -// @public (undocumented) +// @public interface ResourceEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -468,7 +464,7 @@ interface ResourceEntityV1alpha1 extends Entity { export { ResourceEntityV1alpha1 as ResourceEntity }; export { ResourceEntityV1alpha1 }; -// @public (undocumented) +// @public export const resourceEntityV1alpha1Validator: KindValidator; // @public @@ -488,7 +484,7 @@ export function serializeEntityRef( }, ): EntityRef; -// @public (undocumented) +// @public export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; // @public @@ -508,7 +504,7 @@ export function stringifyLocationReference(ref: { target: string; }): string; -// @public (undocumented) +// @public interface SystemEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -523,10 +519,10 @@ interface SystemEntityV1alpha1 extends Entity { export { SystemEntityV1alpha1 as SystemEntity }; export { SystemEntityV1alpha1 }; -// @public (undocumented) +// @public export const systemEntityV1alpha1Validator: KindValidator; -// @public (undocumented) +// @public export interface TemplateEntityV1beta2 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1beta2'; @@ -550,7 +546,7 @@ export interface TemplateEntityV1beta2 extends Entity { }; } -// @public (undocumented) +// @public export const templateEntityV1beta2Validator: KindValidator; // @alpha @@ -569,7 +565,7 @@ export type UNSTABLE_EntityStatusItem = { // @alpha export type UNSTABLE_EntityStatusLevel = 'info' | 'warning' | 'error'; -// @public (undocumented) +// @public interface UserEntityV1alpha1 extends Entity { // (undocumented) apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; @@ -588,10 +584,10 @@ interface UserEntityV1alpha1 extends Entity { export { UserEntityV1alpha1 as UserEntity }; export { UserEntityV1alpha1 }; -// @public (undocumented) +// @public export const userEntityV1alpha1Validator: KindValidator; -// @public (undocumented) +// @public export type Validators = { isValidApiVersion(value: unknown): boolean; isValidKind(value: unknown): boolean; @@ -609,5 +605,5 @@ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; // Warnings were encountered during analysis: // -// src/entity/Entity.d.ts:38:5 - (ae-incompatible-release-tags) The symbol "status" is marked as @public, but its signature references "UNSTABLE_EntityStatus" which is marked as @alpha +// src/entity/Entity.d.ts:41:5 - (ae-incompatible-release-tags) The symbol "status" is marked as @public, but its signature references "UNSTABLE_EntityStatus" which is marked as @alpha ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 45b947c890..615e80b684 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.4", + "version": "0.9.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.13", "ajv": "^7.0.3", @@ -41,7 +42,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.7.15", + "@backstage/cli": "^0.8.0", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 0fff68d569..e5aaeb0c20 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -52,7 +52,11 @@ class AnyEntityPolicy implements EntityPolicy { } } -/** @public */ +/** + * Provides helpers for enforcing a set of {@link EntityPolicy} in an `and`/`or` expression. + * + * @public + */ export const EntityPolicies = { allOf(policies: EntityPolicy[]): EntityPolicy { return new AllEntityPolicies(policies); diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index ca38fef183..9d65d3e8f3 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -14,15 +14,18 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { EntityName } from '../types'; import { UNSTABLE_EntityStatus } from './EntityStatus'; /** * The parts of the format that's common to all versions/kinds of entity. * + * @remarks + * + * See also: + * {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/} * @public - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type Entity = { /** @@ -63,9 +66,13 @@ export type Entity = { /** * Metadata fields common to all versions/kinds of entity. * + * @remarks + * + * See also: + * {@link https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta} + * {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/} + * * @public - * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type EntityMeta = JsonObject & { /** diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index ff031fc1ae..6daf6b78d9 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -33,10 +33,15 @@ export const ENTITY_META_GENERATED_FIELDS = [ ] as const; /** - * Annotations for linking to entity from catalog pages. + * Annotation for linking to entity page from catalog pages. * * @public */ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; -/** @public */ + +/** + * Annotation for linking to entity edit page from catalog pages. + * + * @public + */ export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index b99901b89b..51059328a6 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -27,6 +27,8 @@ import { Entity } from '../Entity'; * Ensures that the format of individual fields of the entity envelope * is valid. * + * @remarks + * * This does not take into account machine generated fields such as uid, etag * and generation. * diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 721dc7d2dc..7f33e71f1c 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -24,6 +24,8 @@ import { EntityPolicy } from './types'; /** * Ensures that the entity spec is valid according to a schema. * + * @remarks + * * This should be the first policy in the list, to ensure that other downstream * policies can work with a structure that is at least valid in therms of the * typescript type. diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 6a38e80dfe..958f750e32 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -69,6 +69,8 @@ export type EntityRefContext = { * Parses an entity reference, either on string or compound form, and always * returns a complete entity name including kind, namespace and name. * + * @remarks + * * This function automatically assumes the default namespace "default" unless * otherwise specified as part of the options, and will throw an error if no * kind was specified in the input reference and no default kind was given. @@ -100,7 +102,9 @@ export function parseEntityName( * Parses an entity reference, either on string or compound form, and returns * a structure with a name, and optional kind and namespace. * - * The options object can contain default values for the kind and namespace, + * @remarks + * + * The context object can contain default values for the kind and namespace, * that will be used if the input reference did not specify any. * * @public @@ -111,12 +115,12 @@ export function parseEntityName( export function parseEntityRef( ref: EntityRef, context?: { defaultKind: string; defaultNamespace: string }, -): { - kind: string; - namespace: string; - name: string; -}; -/** @public */ +): EntityName; +/** + * parseEntityRef with optional Kind. + * + * @public + */ export function parseEntityRef( ref: EntityRef, context?: { defaultKind: string }, @@ -125,7 +129,11 @@ export function parseEntityRef( namespace?: string; name: string; }; -/** @public */ +/** + * parseEntityRef with optional Namespace. + * + * @public + */ export function parseEntityRef( ref: EntityRef, context?: { defaultNamespace: string }, @@ -134,6 +142,11 @@ export function parseEntityRef( namespace: string; name: string; }; +/** + * parseEntityRef with optional Kind and Namespace. + * + * @public + */ export function parseEntityRef( ref: EntityRef, context: EntityRefContext = {}, @@ -223,6 +236,8 @@ export function serializeEntityRef( * Takes an entity or entity name/reference, and returns the string form of an * entity ref. * + * @remarks + * * This function creates a canonical and unique reference to the entity, converting * all parts of the name to lowercase and inserts the default namespace if needed. * It is typically not the best way to represent the entity reference to the user. @@ -256,6 +271,8 @@ export function stringifyEntityRef( /** * Compares an entity to either a string reference or a compound reference. * + * @remarks + * * The comparison is case insensitive, and all of kind, namespace, and name * must match (after applying the optional context to the ref). * diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 314f3b7737..ce6415afde 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -43,6 +43,8 @@ export function generateEntityEtag(): string { * Checks whether there are any significant changes going from the previous to * the next version of this entity. * + * @remarks + * * Significance, in this case, means that we do not compare generated fields * such as uid, etag and generation. * @@ -98,6 +100,8 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { * Takes an old revision of an entity and a new desired state, and merges * them into a complete new state. * + * @remarks + * * The previous revision is expected to be a complete model loaded from the * catalog, including the uid, etag and generation fields. * diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 310b6695eb..d733b03c8c 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -18,7 +18,15 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/API.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage API kind Entity. APIs describe the interfaces for Components to communicate. + * + * @remarks + * + * See {@link https://backstage.io/docs/features/software-catalog/system-model} + * + * @public + */ export interface ApiEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'API'; @@ -31,6 +39,10 @@ export interface ApiEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link ApiEntityV1alpha1}. + * + * @public + */ export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 6777778409..f931eab553 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -18,7 +18,15 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Component.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog Component kind Entity. Represents a single, individual piece of software. + * + * @remarks + * + * See {@link https://backstage.io/docs/features/software-catalog/system-model} + * + * @public + */ export interface ComponentEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Component'; @@ -34,6 +42,10 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link ComponentEntityV1alpha1}. + * + * @public + */ export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index fe777af24e..e0302fcc52 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -18,7 +18,15 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage Domain kind Entity. Domains group Systems together. + * + * @remarks + * + * See {@link https://backstage.io/docs/features/software-catalog/system-model} + * + * @public + */ export interface DomainEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Domain'; @@ -27,6 +35,10 @@ export interface DomainEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link DomainEntityV1alpha1}. + * + * @public + */ export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index f6aab856f0..8d88817dbe 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -18,7 +18,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog Group kind Entity. + * + * @public + */ export interface GroupEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Group'; @@ -35,6 +39,9 @@ export interface GroupEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link GroupEntityV1alpha1}. + * @public + */ export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index f2ae4bcc91..1872e43526 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -18,7 +18,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Location.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog Location kind Entity. + * + * @public + */ export interface LocationEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Location'; @@ -29,6 +33,10 @@ export interface LocationEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link LocationEntityV1alpha1}. + * + * @public + */ export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index acc6ab5245..0a360cb60b 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -18,7 +18,15 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog Resource kind Entity. Represents infrastructure required to operate Components. + * + * @remarks + * + * See {@link https://backstage.io/docs/features/software-catalog/system-model} + * + * @public + */ export interface ResourceEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Resource'; @@ -30,6 +38,10 @@ export interface ResourceEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link ResourceEntityV1alpha1}. + * + * @public + */ export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 5f90351480..7d1ab703a2 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -18,7 +18,15 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/System.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog System kind Entity. Systems group Comopnents, Resources and APIs together. + * + * @remarks + * + * See {@link https://backstage.io/docs/features/software-catalog/system-model} + * + * @public + */ export interface SystemEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'System'; @@ -28,6 +36,10 @@ export interface SystemEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link SystemEntityV1alpha1}. + * + * @public + */ export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index e708d4b387..f4c2dfb5bc 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1beta2.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog Template kind Entity. Templates are used by the Scaffolder plugin to create new Components. + * + * @public + */ export interface TemplateEntityV1beta2 extends Entity { apiVersion: 'backstage.io/v1beta2'; kind: 'Template'; @@ -38,6 +42,10 @@ export interface TemplateEntityV1beta2 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link TemplateEntityV1beta2}. + * + * @public + */ export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index d719ab35ef..82aaebd9d6 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -18,7 +18,11 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; -/** @public */ +/** + * Backstage catalog User kind Entity. + * + * @public + */ export interface UserEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'User'; @@ -32,6 +36,10 @@ export interface UserEntityV1alpha1 extends Entity { }; } -/** @public */ +/** + * {@link KindValidator} for {@link UserEntityV1alpha1}. + * + * @public + */ export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/relations.ts b/packages/catalog-model/src/kinds/relations.ts index 7546d1508b..4d545c9d35 100644 --- a/packages/catalog-model/src/kinds/relations.ts +++ b/packages/catalog-model/src/kinds/relations.ts @@ -24,61 +24,115 @@ Naming rules for relations in priority order: /** * An ownership relation where the owner is usually an organizational - * entity (user or group), and the other entity can be anything. + * entity (user or group), and the other entity can be anything. Reversed + * direction of {@link RELATION_OWNER_OF}. * * @public */ export const RELATION_OWNED_BY = 'ownedBy'; -/** @public */ + +/** + * A relationship from an owner to the owned entity. Reversed direction of + * {@link RELATION_OWNED_BY}. + * + * @public + */ export const RELATION_OWNER_OF = 'ownerOf'; /** - * A relation with an API entity, typically from a component + * A relation with an API entity, typically from a component. Reversed direction of + * {@link RELATION_API_CONSUMED_BY}. * * @public */ export const RELATION_CONSUMES_API = 'consumesApi'; -/** @public */ + +/** + * A relation of an API being consumed, typically by a component. Reversed direction of + * {@link RELATION_CONSUMES_API}. + * + * @public + */ export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; -/** @public */ + +/** + * A relation from an API provider entity (typically a component) to the API. Reversed direction of + * {@link RELATION_API_PROVIDED_BY}. + * + * @public + */ export const RELATION_PROVIDES_API = 'providesApi'; -/** @public */ + +/** + * A relation from an API to its provider entity (typically a component). Reversed direction of + * {@link RELATION_PROVIDES_API}. + * + * @public + */ export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; /** - * A relation denoting a dependency on another entity. + * A relation denoting a dependency on another entity. Reversed direction of + * {@link RELATION_DEPENDENCY_OF}. * * @public */ export const RELATION_DEPENDS_ON = 'dependsOn'; -/** @public */ + +/** + * A relation denoting a reverse dependency by another entity. Reversed direction of + * {@link RELATION_DEPENDS_ON}. + * + * @public + */ export const RELATION_DEPENDENCY_OF = 'dependencyOf'; /** * A parent/child relation to build up a tree, used for example to describe - * the organizational structure between groups. + * the organizational structure between groups. Reversed direction of + * {@link RELATION_CHILD_OF}. * * @public */ export const RELATION_PARENT_OF = 'parentOf'; -/** @public */ + +/** + * A relation from a child to a parent entity, used for example to describe + * the organizational structure between groups. Reversed direction of + * {@link RELATION_PARENT_OF}. + * + * @public + */ export const RELATION_CHILD_OF = 'childOf'; /** - * A membership relation, typically for users in a group. + * A membership relation, typically for users in a group. Reversed direction of + * {@link RELATION_HAS_MEMBER}. * * @public */ export const RELATION_MEMBER_OF = 'memberOf'; -/** @public */ + +/** + * A relation from a group to its member, typcally a user in a group. Reversed direction of + * {@link RELATION_MEMBER_OF}. + * + * @public + */ export const RELATION_HAS_MEMBER = 'hasMember'; /** * A part/whole relation, typically for components in a system and systems - * in a domain. + * in a domain. Reversed direction of {@link RELATION_HAS_PART}. * * @public */ export const RELATION_PART_OF = 'partOf'; -/** @public */ + +/** + * A relation from a containing entity to a contained entity. Reversed direction of + * {@link RELATION_PART_OF}. + * + * @public + */ export const RELATION_HAS_PART = 'hasPart'; diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 4bcabdb705..8eb393f555 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -14,11 +14,20 @@ * limitations under the License. */ -/** @public */ +/** + * Constant storing location annotation. + * + * @public */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; -/** @public */ +/** + * Constant storing origin location annotation + * + * @public */ export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; -/** @public */ +/** + * Contant storing source location annotation + * + * @public */ export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index cf12031dc1..1edd4e5ec5 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -65,6 +65,8 @@ export function parseLocationReference(ref: string): { /** * Turns a location reference into its string form. * + * @remarks + * * Note that the input type is not `LocationSpec`, because we do not want to * conflate the string form with the additional properties of that type. * @@ -90,6 +92,8 @@ export function stringifyLocationReference(ref: { /** * Returns the source code location of the Entity, to the extent that one exists. * + * @remarks + * * If the returned location type is of type 'url', the target should be readable at least * using the UrlReader from `@backstage/backend-common`. If it is not of type 'url', the caller * needs to have explicit handling of each location type or signal that it is not supported. diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 1d16a9047d..725f873f2d 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -14,17 +14,28 @@ * limitations under the License. */ -/** @public */ +/** + * Holds the entity location information. + * + * @remarks + * + * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. + * This flag is then set to indicate that the file can be not present. + * default value: 'required'. + * + * @public + */ export type LocationSpec = { type: string; target: string; - // When using repo importer plugin, location is being created before the component yaml file is merged to the main branch. - // This flag is then set to indicate that the file can be not present. - // default value: 'required'. presence?: 'optional' | 'required'; }; -/** @public */ +/** + * Entity location for a specific entity. + * + * @public + */ export type Location = { id: string; } & LocationSpec; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 452c69af53..2fd692e649 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -18,8 +18,10 @@ import * as yup from 'yup'; import { LocationSpec, Location } from './types'; /** + * Deprecated. + * * @public - * @deprecated Use JSONSchema or validators instead. + * @deprecated Use {@link JSONSchema} or validators instead. */ export const locationSpecSchema: yup.SchemaOf = yup .object({ @@ -31,8 +33,10 @@ export const locationSpecSchema: yup.SchemaOf = yup .required(); /** + * Deprecated. + * * @public - * @deprecated Use JSONSchema or validators instead. + * @deprecated Use {@link JSONSchema} or validators instead. */ export const locationSchema: yup.SchemaOf = yup .object({ @@ -45,8 +49,10 @@ export const locationSchema: yup.SchemaOf = yup .required(); /** + * Deprecated. + * * @public - * @deprecated Use JSONSchema or validators instead. + * @deprecated Use {@link JSONSchema} or validators instead. */ export const analyzeLocationSchema: yup.SchemaOf<{ location: LocationSpec }> = yup diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 1fbb3be95c..1a0fab267c 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; -/** @public */ +/** + * JSONSchema extendable by arbitrary JSON attributes + * + * @public + */ export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; /** @@ -35,7 +39,9 @@ export type EntityName = { * A reference by name to an entity, either as a compact string representation, * or as a compound reference structure. * - * The string representation is on the form [:][/]. + * @remarks + * + * The string representation is on the form `[:][/]`. * * Left-out parts of the reference need to be handled by the application, * either by rejecting the reference or by falling back to default values. diff --git a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts index daa44ffaf1..a2a8ba64b7 100644 --- a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts @@ -25,6 +25,8 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * if it matches that schema, or throws a {@link globals#TypeError} describing the * errors. * + * @remarks + * * Note that this validator is only meant for applying the base schema checks; * it does not take custom policies or additional processor based validation * into account. @@ -33,13 +35,15 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * own, it may contain `$ref` references to the following, which are resolved * automatically for you: * - * - EntityEnvelope - * - Entity - * - EntityMeta - * - common# + * - {@link EntityEnvelope} + * - {@link Entity} + * - {@link EntityMeta} + * - `common#` + * + * See also {@link https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema} * * @public - * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema + * */ export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts index 535dba9842..66259cc63d 100644 --- a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts @@ -24,6 +24,8 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * schema apiVersion/kind didn't apply to that data, or throws a * {@link globals#TypeError} describing actual errors. * + * @remarks + * * This validator is highly specialized, in that it has special treatment of * the `kind` and `apiVersion` root keys. This only works if your schema has * their rule set to `"enum"`: @@ -47,13 +49,16 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * The given schema may contain `$ref` references to the following, which are * resolved automatically for you: * - * - EntityEnvelope - * - Entity - * - EntityMeta - * - common# + * - {@link Entity} + * + * - {@link EntityEnvelope} + * + * - {@link EntityMeta} + * + * - `common#` + * @see {@link https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema} * * @public - * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entityKindSchemaValidator( schema: unknown, diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.ts b/packages/catalog-model/src/validation/entitySchemaValidator.ts index dd110efd75..fc1918e566 100644 --- a/packages/catalog-model/src/validation/entitySchemaValidator.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.ts @@ -24,21 +24,23 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * returns that data cast to an {@link Entity} (or the given subtype) if it * matches that schema, or throws a {@link globals#TypeError} describing the errors. * + * @remarks + * * Note that this validator is only meant for applying the base schema checks; * it does not take custom policies or additional processor based validation * into account. * - * By default, the plain `Entity` schema is used. If you pass in your own, it + * By default, the plain {@link Entity} schema is used. If you pass in your own, it * may contain `$ref` references to the following, which are resolved * automatically for you: * - * - EntityEnvelope - * - Entity - * - EntityMeta - * - common# + * - {@link Entity} + * - {@link EntityEnvelope} + * - {@link EntityMeta} + * - `common#` * * @public - * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema + * @see {@link https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema} */ export function entitySchemaValidator( schema?: unknown, diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index bd6dccc419..ad3f52e563 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -30,7 +30,11 @@ const defaultValidators: Validators = { isValidTag: CommonValidatorFunctions.isValidTag, }; -/** @public */ +/** + * Creates a {@link Validators} object from `overrides`, with default values taken from {@link KubernetesValidatorFunctions} + * + * @public + */ export function makeValidator(overrides: Partial = {}): Validators { return { ...defaultValidators, diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index 23d639c166..e3615e6a83 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -/** @public */ +/** + * Type alias for implementing validators of various entity objects. + * + * @public + */ export type Validators = { isValidApiVersion(value: unknown): boolean; isValidKind(value: unknown): boolean; diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 894060f2e8..5c1cf8af08 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -46,6 +46,8 @@ describe('paths', () => { const dir = resolvePath(__dirname, '..'); const root = resolvePath(__dirname, '../../..'); + jest.spyOn(process, 'cwd').mockReturnValue(dir); + const paths = findPaths(__dirname); expect(paths.ownDir).toBe(dir); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index b88175ee24..7cb3c5be91 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/cli +## 0.8.0 + +### Minor Changes + +- b486adb8c6: The Jest configuration that's included with the Backstage CLI has received several changes. + + As a part of migrating to more widespread usage of ESM modules, the default configuration now transforms all source files everywhere, including those within `node_modules`. Due to this change the existing `transformModules` option has been removed and will be ignored. There is also a list of known packages that do not require transforms in the CLI, which will evolve over time. If needed there will also be an option to add packages to this list in the future, but it is not included yet to avoid clutter. + + To counteract the slowdown of the additional transforms that have been introduced, the default configuration has also been reworked to enable caching across different packages. Previously each package in a Backstage monorepo would have its own isolated Jest cache, but it is now shared between packages that have a similar enough Jest configuration. + + Another change that will speed up test execution is that the transformer for `.esm.js` files has been switched. It used to be an ESM transformer based on Babel, but it is also done by sucrase now since it is significantly faster. + + The changes above are not strictly breaking as all tests should still work. It may however cause excessive slowdowns in projects that have configured custom transforms in the `jest` field within `package.json` files. In this case it is either best to consider removing the custom transforms, or overriding the `transformIgnorePatterns` to instead use Jest's default `'/node_modules/'` pattern. + + This change also removes the `@backstage/cli/config/jestEsmTransform.js` transform, which can be replaced by using the `@backstage/cli/config/sucraseEsmTransform.js` transform instead. + +### Patch Changes + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/config-loader@0.7.0 + - @backstage/errors@0.1.3 + +## 0.7.16 + +### Patch Changes + +- 53bdc66623: add a --from option to the plugin install command +- 84e24fcdaf: Bump sucrase to version 3.20.2 +- 6583c6ac40: Add semicolon in template to make prettier happy +- c6f927d819: Bump mini-css-extract-plugin to v2 +- 16f044cb6b: Update default backend ESLint configuration to allow usage of `__dirname` in tests. +- 1ef9e64901: Add an experimental `install ` command. + + Given a `pluginId`, the command looks for NPM packages matching `@backstage/plugin-{pluginId}` or `backstage-plugin-{pluginId}` or `{pluginId}`. It looks for the `experimentalInstallationRecipe` in their `package.json` for the steps of installation. Detailed documentation and API Spec to follow (and to be decided as well). + ## 0.7.15 ### Patch Changes diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 99767f5d79..a97f26f8da 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -14,6 +14,15 @@ * limitations under the License. */ +const globalRestrictedSyntax = [ + { + message: + 'Default import from winston is not allowed, import `* as winston` instead.', + selector: + 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', + }, +]; + module.exports = { extends: [ '@spotify/eslint-config-base', @@ -69,17 +78,12 @@ module.exports = { // Avoid default import from winston as it breaks at runtime 'no-restricted-syntax': [ 'error', - { - message: - 'Default import from winston is not allowed, import `* as winston` instead.', - selector: - 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', - }, { message: "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", selector: 'Identifier[name="__dirname"]', }, + ...globalRestrictedSyntax, ], }, overrides: [ @@ -103,6 +107,7 @@ module.exports = { bundledDependencies: true, }, ], + 'no-restricted-syntax': ['error', ...globalRestrictedSyntax], }, }, ], diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 80b3e84cda..e889d23c3a 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -16,9 +16,27 @@ const fs = require('fs-extra'); const path = require('path'); +const crypto = require('crypto'); const glob = require('util').promisify(require('glob')); +const { version } = require('../package.json'); -async function getProjectConfig(targetPath) { +const transformIgnorePattern = [ + '@material-ui', + '@rjsf', + 'ajv', + 'core-js', + 'jest-.*', + 'jsdom', + 'knex', + 'react', + 'react-dom', + 'highlight\\.js', + 'prismjs', + 'react-use', + 'typescript', +].join('|'); + +async function getProjectConfig(targetPath, displayName) { const configJsPath = path.resolve(targetPath, 'jest.config.js'); const configTsPath = path.resolve(targetPath, 'jest.config.ts'); // If the package has it's own jest config, we use that instead. @@ -58,10 +76,7 @@ async function getProjectConfig(targetPath) { currentPath = newPath; } - // We add an additional Jest config parameter only known by the Backstage CLI - // called `transformModules`. It's a list of modules that we want to apply - // our configured jest transformations for. - // This is useful when packages are published in untranspiled ESM or TS form. + // This is an old deprecated option that is no longer used. const transformModules = pkgJsonConfigs .flatMap(conf => { const modules = conf.transformModules || []; @@ -70,9 +85,14 @@ async function getProjectConfig(targetPath) { }) .map(name => `${name}/`) .join('|'); - const transformModulePattern = transformModules && `(?!${transformModules})`; + if (transformModules.length > 0) { + console.warn( + 'The Backstage CLI jest transformModules option is no longer used and will be ignored. All modules are now always transformed.', + ); + } const options = { + ...(displayName && { displayName }), rootDir: path.resolve(targetPath, 'src'), coverageDirectory: path.resolve(targetPath, 'coverage'), collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], @@ -81,8 +101,7 @@ async function getProjectConfig(targetPath) { }, transform: { - '\\.esm\\.js$': require.resolve('./jestEsmTransform.js'), // See jestEsmTransform.js - '\\.(js|jsx|ts|tsx)$': require.resolve('@sucrase/jest-plugin'), + '\\.(js|jsx|ts|tsx)$': require.resolve('./jestSucraseTransform.js'), '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('jest-transform-yaml'), @@ -91,11 +110,7 @@ async function getProjectConfig(targetPath) { // A bit more opinionated testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'], - // Default behaviour is to not apply transforms for node_modules, but we still want - // to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages. - transformIgnorePatterns: [ - `/node_modules/${transformModulePattern}.*\\.(?:(?/setupTests.ts']; } - return Object.assign(options, ...pkgJsonConfigs); + const config = Object.assign(options, ...pkgJsonConfigs); + + // The config name is a cache key that lets us share the jest cache across projects. + // If no explicit name was configured, generated one based on the configuration. + if (!config.name) { + const configHash = crypto + .createHash('md5') + .update(version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config.transform)) + .digest('hex'); + config.name = `backstage_cli_${configHash}`; + } + + return config; } // This loads the root jest config, which in turn will either refer to a single @@ -143,7 +172,7 @@ async function getRootConfig() { const packageData = await fs.readJson(packagePath); const testScript = packageData.scripts && packageData.scripts.test; if (testScript && testScript.includes('backstage-cli test')) { - return await getProjectConfig(projectPath); + return await getProjectConfig(projectPath, packageData.name); } return undefined; diff --git a/packages/cli/config/jestEsmTransform.js b/packages/cli/config/jestEsmTransform.js index 742822274d..c1eec7d9f8 100644 --- a/packages/cli/config/jestEsmTransform.js +++ b/packages/cli/config/jestEsmTransform.js @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2021 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,6 @@ * limitations under the License. */ -const babel = require('@babel/core'); - -// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed -// TODO: jest is working on module support, it's possible that we can remove this in the future -module.exports = { - process(src) { - const result = babel.transform(src, { - babelrc: false, - compact: false, - plugins: [ - // This transforms the regular ESM syntax, import and export statements - require.resolve('@babel/plugin-transform-modules-commonjs'), - // This transforms dynamic `import()`, which is not supported yet in the Node.js VM API - require.resolve('babel-plugin-dynamic-import-node'), - ], - }); - - return result.code; - }, -}; +throw new Error( + 'The ./jestEsmTransform.js transform has been removed, switch to using ./jestSucraseTransform.js instead', +); diff --git a/packages/cli/config/jestSucraseTransform.js b/packages/cli/config/jestSucraseTransform.js new file mode 100644 index 0000000000..01acfee38d --- /dev/null +++ b/packages/cli/config/jestSucraseTransform.js @@ -0,0 +1,66 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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. + */ + +const { createHash } = require('crypto'); +const { transform } = require('sucrase'); +const sucrasePkg = require('sucrase/package.json'); +const sucrasePluginPkg = require('@sucrase/jest-plugin/package.json'); + +const ESM_REGEX = /\b(?:import|export)\b/; + +function process(source, filePath) { + let transforms; + + if (filePath.endsWith('.esm.js')) { + transforms = ['imports']; + } else if (filePath.endsWith('.js')) { + // This is a very rough filter to avoid transforming things that we quickly + // can be sure are definitely not ESM modules. + if (ESM_REGEX.test(source)) { + transforms = ['imports', 'jsx']; // JSX within .js is currently allowed + } + } else if (filePath.endsWith('.jsx')) { + transforms = ['jsx', 'imports']; + } else if (filePath.endsWith('.ts')) { + transforms = ['typescript', 'imports']; + } else if (filePath.endsWith('.tsx')) { + transforms = ['typescript', 'jsx', 'imports']; + } + + // Only apply the jest transform to the test files themselves + if (transforms && filePath.includes('.test.')) { + transforms.push('jest'); + } + + if (transforms) { + return transform(source, { transforms, filePath }).code; + } + + return source; +} + +// TODO: contribute something like this to @sucrase/jest-plugin +function getCacheKey(sourceText) { + return createHash('md5') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(sucrasePkg.version) + .update(Buffer.alloc(1)) + .update(sucrasePluginPkg.version) + .digest('hex'); +} + +module.exports = { process, getCacheKey }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 53c1b06e76..62a008087e 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.7.15", + "version": "0.8.0", "private": false, "publishConfig": { "access": "public" @@ -28,11 +28,11 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@babel/core": "^7.4.4", - "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.10", - "@backstage/config-loader": "^0.6.10", + "@backstage/config-loader": "^0.7.0", + "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -54,7 +54,6 @@ "@typescript-eslint/eslint-plugin": "^v4.30.0", "@typescript-eslint/parser": "^v4.28.3", "@yarnpkg/lockfile": "^1.1.0", - "babel-plugin-dynamic-import-node": "^2.3.3", "bfj": "^7.0.2", "buffer": "^6.0.3", "chalk": "^4.0.0", @@ -85,7 +84,7 @@ "json-schema": "^0.3.0", "jest-transform-yaml": "^0.1.1", "lodash": "^4.17.21", - "mini-css-extract-plugin": "^1.4.1", + "mini-css-extract-plugin": "^2.4.2", "node-libs-browser": "^2.2.1", "ora": "^5.3.0", "postcss": "^8.1.0", @@ -118,14 +117,14 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.6", + "@backstage/backend-common": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.6.1", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/core-app-api": "^0.1.16", - "@backstage/dev-utils": "^0.2.11", - "@backstage/test-utils": "^0.1.18", - "@backstage/theme": "^0.2.10", + "@backstage/core-components": "^0.7.1", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/core-app-api": "^0.1.18", + "@backstage/dev-utils": "^0.2.12", + "@backstage/test-utils": "^0.1.19", + "@backstage/theme": "^0.2.11", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/config/docs.ts b/packages/cli/src/commands/config/docs.ts index 198244b941..7a807f7c78 100644 --- a/packages/cli/src/commands/config/docs.ts +++ b/packages/cli/src/commands/config/docs.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; import { Command } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/commands/config/schema.ts index a36fc24f06..be228e1e7c 100644 --- a/packages/cli/src/commands/config/schema.ts +++ b/packages/cli/src/commands/config/schema.ts @@ -18,7 +18,7 @@ import { Command } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../../lib/config'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; export default async (cmd: Command) => { diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 6ddd1f7160..a5ea565cae 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -24,6 +24,7 @@ import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import os from 'os'; import { Command } from 'commander'; +import { assertError } from '@backstage/errors'; import { parseOwnerIds, addCodeownersEntry, @@ -173,6 +174,7 @@ async function buildPlugin(pluginFolder: string) { ); }); } catch (error) { + assertError(error); Task.error(error.message); break; } @@ -329,6 +331,7 @@ export default async (cmd: Command) => { Task.log(); Task.exit(); } catch (error) { + assertError(error); Task.error(error.message); Task.log('It seems that something went wrong when creating the plugin πŸ€”'); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index d6686f2655..302f76cc5d 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { assertError } from '@backstage/errors'; import { CommanderStatic } from 'commander'; import { exitWithError } from '../lib/errors'; @@ -252,6 +253,7 @@ function lazy( process.exit(0); } catch (error) { + assertError(error); exitWithError(error); } }; diff --git a/packages/cli/src/commands/install/types.ts b/packages/cli/src/commands/install/types.ts index 5479032de5..3105f78822 100644 --- a/packages/cli/src/commands/install/types.ts +++ b/packages/cli/src/commands/install/types.ts @@ -15,7 +15,7 @@ */ import { YarnInfoInspectData } from '../../lib/versioning'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * TODO: possible types diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 9e1e18bb7b..11fdc8bb11 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -20,6 +20,7 @@ import inquirer, { Answers, Question } from 'inquirer'; import { getCodeownersFilePath } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; import { Task } from '../../lib/tasks'; +import { assertError } from '@backstage/errors'; const BACKSTAGE = '@backstage'; @@ -35,6 +36,7 @@ export const checkExists = async (rootDir: string, pluginName: string) => { ); } } catch (e) { + assertError(e); throw new Error( chalk.red( ` There was an error removing plugin ${chalk.cyan(pluginName)}: ${ @@ -51,6 +53,7 @@ export const removePluginDirectory = async (destination: string) => { try { await fse.remove(destination); } catch (e) { + assertError(e); throw Error( chalk.red( ` There was a problem removing the plugin directory: ${e.message}`, @@ -67,6 +70,7 @@ export const removeSymLink = async (destination: string) => { try { await fse.remove(destination); } catch (e) { + assertError(e); throw Error( chalk.red( ` Could not remove symbolic link\t${chalk.cyan(destination)}: ${ @@ -106,6 +110,7 @@ export const removeReferencesFromPluginsFile = async ( try { await removeAllStatementsContainingID(pluginsFile, pluginNameCapitalized); } catch (e) { + assertError(e); throw new Error( chalk.red( ` There was an error removing export statement for plugin ${chalk.cyan( @@ -125,6 +130,7 @@ export const removePluginFromCodeOwners = async ( try { await removeAllStatementsContainingID(codeOwnersFile, pluginName); } catch (e) { + assertError(e); throw new Error( chalk.red( ` There was an error removing code owners statement for plugin ${chalk.cyan( @@ -165,6 +171,7 @@ export const removeReferencesFromAppPackage = async ( 'utf-8', ); } catch (e) { + assertError(e); throw new Error( chalk.red( ` Failed to remove plugin as dependency in app: ${chalk.cyan( @@ -245,6 +252,7 @@ export default async () => { ); Task.log(); } catch (error) { + assertError(error); Task.error(error.message); Task.log('It seems that something went wrong when removing the plugin πŸ€”'); } diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 257a83e2be..7b967839b2 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -17,6 +17,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import semver from 'semver'; +import { isError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { run } from '../../lib/run'; import { paths } from '../../lib/paths'; @@ -59,7 +60,7 @@ export default async () => { try { target = await findTargetVersion(name); } catch (error) { - if (error.name === 'NotFoundError') { + if (isError(error) && error.name === 'NotFoundError') { console.log(`Package info not found, ignoring package ${name}`); return; } @@ -97,7 +98,7 @@ export default async () => { try { target = await findTargetVersion(name); } catch (error) { - if (error.name === 'NotFoundError') { + if (isError(error) && error.name === 'NotFoundError') { console.log(`Package info not found, ignoring package ${name}`); return; } diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index b8164d263d..254799f78c 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -26,7 +26,7 @@ export const optimization = ( return { minimize: !isDev, - // Only configure when parallel is explicitly overriden from the default + // Only configure when parallel is explicitly overridden from the default ...(!isParallelDefault(options.parallel) ? { minimizer: [ @@ -45,11 +45,15 @@ export const optimization = ( // enough, if they're smaller they end up in the main packages: { chunks: 'initial', - test: /[\\/]node_modules[\\/]/, + test(module: any) { + return Boolean( + module?.resource?.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/), + ); + }, name(module: any) { // get the name. E.g. node_modules/packageName/not/this/part.js // or node_modules/packageName - const packageName = module.context.match( + const packageName = module.resource.match( /[\\/]node_modules[\\/](.*?)([\\/]|$)/, )[1]; diff --git a/packages/cli/src/lib/run.ts b/packages/cli/src/lib/run.ts index 7cf22a6df9..4efb791c30 100644 --- a/packages/cli/src/lib/run.ts +++ b/packages/cli/src/lib/run.ts @@ -23,6 +23,7 @@ import { import { ExitCodeError } from './errors'; import { promisify } from 'util'; import { LogFunc } from './logging'; +import { assertError, ForwardedError } from '@backstage/errors'; const execFile = promisify(execFileCb); @@ -75,10 +76,14 @@ export async function runPlain(cmd: string, ...args: string[]) { const { stdout } = await execFile(cmd, args, { shell: true }); return stdout.trim(); } catch (error) { - if (error.stderr) { - process.stderr.write(error.stderr); + assertError(error); + if ('stderr' in error) { + process.stderr.write(error.stderr as Buffer); } - throw new ExitCodeError(error.code, [cmd, ...args].join(' ')); + if (typeof error.code === 'number') { + throw new ExitCodeError(error.code, [cmd, ...args].join(' ')); + } + throw new ForwardedError('Unknown execution error', error); } } diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 292b0cc471..48c09b5346 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1,2 +1,2 @@ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill' +import 'cross-fetch/polyfill'; diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index d038e16ccc..7aabaef583 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/codemods +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-app-api@0.1.18 + - @backstage/core-plugin-api@0.1.11 + +## 0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.17 + - @backstage/core-components@0.7.0 + ## 0.1.17 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index a745498613..77bdd181be 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.17", + "version": "0.1.19", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 34fc34b441..0b4ef9a5f3 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/config-loader +## 0.7.0 + +### Minor Changes + +- 7e97d0b8c1: Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise`. + +### Patch Changes + +- 223e8de6b4: Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds. +- 7e97d0b8c1: Add public tags and documentation +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + ## 0.6.10 ### Patch Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 355839c5af..19b5ee9e27 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -4,7 +4,7 @@ ```ts import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; // @public @@ -26,18 +26,15 @@ export type ConfigSchemaProcessingOptions = { // @public export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; -// @public (undocumented) -export type EnvFunc = (name: string) => Promise; - // @public export function loadConfig(options: LoadConfigOptions): Promise; -// @public (undocumented) +// @public export type LoadConfigOptions = { configRoot: string; configPaths: string[]; env?: string; - experimentalEnvFunc?: EnvFunc; + experimentalEnvFunc?: (name: string) => Promise; watch?: { onChange: (configs: AppConfig[]) => void; stopSignal?: Promise; @@ -49,7 +46,7 @@ export function loadConfigSchema( options: LoadConfigSchemaOptions, ): Promise; -// @public (undocumented) +// @public export type LoadConfigSchemaOptions = | { dependencies: string[]; diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index d423c915d0..a731225e2b 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.10", + "version": "0.7.0", "private": false, "publishConfig": { "access": "public", @@ -32,12 +32,15 @@ "dependencies": { "@backstage/cli-common": "^0.1.4", "@backstage/config": "^0.1.9", + "@backstage/errors": "^0.1.3", + "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", "fs-extra": "9.1.0", "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", + "json-schema-traverse": "^1.0.0", "typescript-json-schema": "^0.50.1", "yaml": "^1.9.2", "yup": "^0.32.9" diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index 0e66c59ec4..888c68ef70 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -25,7 +25,6 @@ export type { ConfigSchema, ConfigSchemaProcessingOptions, ConfigVisibility, - EnvFunc, LoadConfigSchemaOptions, TransformFunc, } from './lib'; diff --git a/packages/config-loader/src/lib/env.ts b/packages/config-loader/src/lib/env.ts index b244c06c64..638b49dbc8 100644 --- a/packages/config-loader/src/lib/env.ts +++ b/packages/config-loader/src/lib/env.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { assertError } from '@backstage/errors'; const ENV_PREFIX = 'APP_CONFIG_'; @@ -96,6 +98,7 @@ function safeJsonParse(str: string): [Error | null, any] { try { return [null, JSON.parse(str)]; } catch (err) { + assertError(err); return [err, str]; } } diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index f8b134119b..8e7f582e44 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -23,7 +23,8 @@ import { } from 'path'; import { ConfigSchemaPackageEntry } from './types'; import { getProgramFromFiles, generateSchema } from 'typescript-json-schema'; -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { assertError } from '@backstage/errors'; type Item = { name?: string; @@ -189,6 +190,7 @@ function compileTsSchemas(paths: string[]) { [path.split(sep).join('/')], // Unix paths are expected for all OSes here ) as JsonObject | null; } catch (error) { + assertError(error); if (error.message !== 'type Config not found') { throw error; } diff --git a/packages/config-loader/src/lib/schema/compile.test.ts b/packages/config-loader/src/lib/schema/compile.test.ts index c9330a0440..1e63f3cbf8 100644 --- a/packages/config-loader/src/lib/schema/compile.test.ts +++ b/packages/config-loader/src/lib/schema/compile.test.ts @@ -29,12 +29,30 @@ describe('compileConfigSchemas', () => { }, ]); expect(validate([{ data: { a: 1 }, context: 'test' }])).toEqual({ - errors: ['Config should be string { type=string } at /a'], - visibilityByPath: new Map(), + errors: [ + { + keyword: 'type', + dataPath: '/a', + schemaPath: '#/properties/a/type', + message: 'should be string', + params: { type: 'string' }, + }, + ], + visibilityByDataPath: new Map(), + visibilityBySchemaPath: new Map(), }); expect(validate([{ data: { b: 'b' }, context: 'test' }])).toEqual({ - errors: ['Config should be number { type=number } at /b'], - visibilityByPath: new Map(), + errors: [ + { + keyword: 'type', + dataPath: '/b', + schemaPath: '#/properties/b/type', + message: 'should be number', + params: { type: 'number' }, + }, + ], + visibilityByDataPath: new Map(), + visibilityBySchemaPath: new Map(), }); }); @@ -78,7 +96,7 @@ describe('compileConfigSchemas', () => { { data: { a: 'a', b: 'b', c: 'c', d: ['d'] }, context: 'test' }, ]), ).toEqual({ - visibilityByPath: new Map( + visibilityByDataPath: new Map( Object.entries({ '/a': 'frontend', '/b': 'secret', @@ -86,6 +104,14 @@ describe('compileConfigSchemas', () => { '/d/0': 'frontend', }), ), + visibilityBySchemaPath: new Map( + Object.entries({ + '/properties/a': 'frontend', + '/properties/b': 'secret', + '/properties/d': 'secret', + '/properties/d/items': 'frontend', + }), + ), }); }); diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index a36d2eb9a2..1cbf5a404d 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -17,6 +17,7 @@ import Ajv from 'ajv'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import mergeAllOf, { Resolvers } from 'json-schema-merge-allof'; +import traverse from 'json-schema-traverse'; import { ConfigReader } from '@backstage/config'; import { ConfigSchemaPackageEntry, @@ -38,7 +39,7 @@ export function compileConfigSchemas( // The ajv instance below is stateful and doesn't really allow for additional // output during validation. We work around this by having this extra piece // of state that we reset before each validation. - const visibilityByPath = new Map(); + const visibilityByDataPath = new Map(); const ajv = new Ajv({ allErrors: true, @@ -62,7 +63,7 @@ export function compileConfigSchemas( /\['?(.*?)'?\]/g, (_, segment) => `/${segment}`, ); - visibilityByPath.set(normalizedPath, visibility); + visibilityByDataPath.set(normalizedPath, visibility); } return true; }; @@ -80,27 +81,30 @@ export function compileConfigSchemas( const merged = mergeConfigSchemas(schemas.map(_ => _.value)); const validate = ajv.compile(merged); + const visibilityBySchemaPath = new Map(); + traverse(merged, (schema, path) => { + if (schema.visibility && schema.visibility !== 'backend') { + visibilityBySchemaPath.set(path, schema.visibility); + } + }); + return configs => { const config = ConfigReader.fromConfigs(configs).get(); - visibilityByPath.clear(); + visibilityByDataPath.clear(); const valid = validate(config); if (!valid) { - const errors = validate.errors ?? []; return { - errors: errors.map(({ dataPath, message, params }) => { - const paramStr = Object.entries(params) - .map(([name, value]) => `${name}=${value}`) - .join(' '); - return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; - }), - visibilityByPath: new Map(), + errors: validate.errors ?? [], + visibilityByDataPath: new Map(visibilityByDataPath), + visibilityBySchemaPath, }; } return { - visibilityByPath: new Map(visibilityByPath), + visibilityByDataPath: new Map(visibilityByDataPath), + visibilityBySchemaPath, }; }; } diff --git a/packages/config-loader/src/lib/schema/filtering.test.ts b/packages/config-loader/src/lib/schema/filtering.test.ts index d4b3cd55b6..0395c7da6a 100644 --- a/packages/config-loader/src/lib/schema/filtering.test.ts +++ b/packages/config-loader/src/lib/schema/filtering.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { JsonObject } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { ConfigVisibility } from './types'; -import { filterByVisibility } from './filtering'; +import { filterByVisibility, filterErrorsByVisibility } from './filtering'; const data = { arr: ['f', 'b', 's'], @@ -175,3 +175,195 @@ describe('filterByVisibility', () => { ).toEqual(expected); }); }); + +describe('filterErrorsByVisibility', () => { + it('should allow empty input', () => { + expect( + filterErrorsByVisibility(undefined, ['frontend'], new Map(), new Map()), + ).toEqual([]); + expect( + filterErrorsByVisibility( + ['my-error' as any], + undefined, + new Map(), + new Map(), + ), + ).toEqual(['my-error']); + expect( + filterErrorsByVisibility([], ['frontend'], new Map(), new Map()), + ).toEqual([]); + }); + + it('should filter generic errors', () => { + const errors = [ + { + keyword: 'something', + dataPath: '/a', + schemaPath: '#/properties/a/something', + params: {}, + message: 'a', + }, + { + keyword: 'something', + dataPath: '/b', + schemaPath: '#/properties/b/something', + params: {}, + message: 'b', + }, + { + keyword: 'something', + dataPath: '/c', + schemaPath: '#/properties/c/something', + params: {}, + message: 'c', + }, + ]; + const visibilityByDataPath = new Map([ + ['/a', 'frontend'], + ['/c', 'secret'], + ]); + + expect( + filterErrorsByVisibility( + errors, + undefined, + visibilityByDataPath, + new Map(), + ), + ).toEqual([ + expect.objectContaining({ message: 'a' }), + expect.objectContaining({ message: 'b' }), + expect.objectContaining({ message: 'c' }), + ]); + expect( + filterErrorsByVisibility( + errors, + ['frontend'], + visibilityByDataPath, + new Map(), + ), + ).toEqual([expect.objectContaining({ message: 'a' })]); + expect( + filterErrorsByVisibility( + errors, + ['backend'], + visibilityByDataPath, + new Map(), + ), + ).toEqual([expect.objectContaining({ message: 'b' })]); + expect( + filterErrorsByVisibility( + errors, + ['secret'], + visibilityByDataPath, + new Map(), + ), + ).toEqual([expect.objectContaining({ message: 'c' })]); + expect( + filterErrorsByVisibility(errors, [], visibilityByDataPath, new Map()), + ).toEqual([]); + }); + + it('should always forward structural type errors', () => { + const errors = [ + { + keyword: 'type', + dataPath: '/a', + schemaPath: '#/properties/a/type', + params: { type: 'number' }, + message: 'a', + }, + { + keyword: 'type', + dataPath: '/b', + schemaPath: '#/properties/b/type', + params: { type: 'string' }, + message: 'b', + }, + { + keyword: 'type', + dataPath: '/c', + schemaPath: '#/properties/c/type', + params: { type: 'array' }, + message: 'c', + }, + { + keyword: 'type', + dataPath: '/c', + schemaPath: '#/properties/c/type', + params: { type: 'object' }, + message: 'd', + }, + { + keyword: 'type', + dataPath: '/c', + schemaPath: '#/properties/c/type', + params: { type: 'null' }, + message: 'e', + }, + ]; + const visibilityByDataPath = new Map([ + ['/a', 'secret'], + ['/b', 'secret'], + ['/c', 'secret'], + ['/d', 'secret'], + ['/e', 'secret'], + ]); + + expect( + filterErrorsByVisibility( + errors, + ['frontend'], + visibilityByDataPath, + new Map(), + ), + ).toEqual([ + expect.objectContaining({ message: 'c' }), + expect.objectContaining({ message: 'd' }), + ]); + }); + + it('should filter requirement errors based on schema path', () => { + const errors = [ + { + keyword: 'required', + dataPath: '/a', + schemaPath: '#/properties/o/required', + params: { missingProperty: 'a' }, + message: 'a', + }, + { + keyword: 'required', + dataPath: '/b', + schemaPath: '#/properties/o/required', + params: { missingProperty: 'b' }, + message: 'b', + }, + { + keyword: 'required', + dataPath: '/c', + schemaPath: '#/properties/o/required', + params: { missingProperty: 'c' }, + message: 'c', + }, + ]; + const visibilityBySchemaPath = new Map([ + ['/properties/o', 'secret'], + ['/properties/o/properties/a', 'frontend'], + ['/properties/o/properties/b', 'secret'], + ['/properties/o/properties/c/properties/x', 'frontend'], + ]); + + expect( + filterErrorsByVisibility( + errors, + ['frontend'], + new Map(), + visibilityBySchemaPath, + ), + ).toEqual([ + expect.objectContaining({ message: 'a' }), + expect.objectContaining({ message: 'c' }), + ]); + }); +}); diff --git a/packages/config-loader/src/lib/schema/filtering.ts b/packages/config-loader/src/lib/schema/filtering.ts index 3533be01b7..b4226d7dad 100644 --- a/packages/config-loader/src/lib/schema/filtering.ts +++ b/packages/config-loader/src/lib/schema/filtering.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { ConfigVisibility, DEFAULT_CONFIG_VISIBILITY, TransformFunc, + ValidationError, } from './types'; /** @@ -28,7 +29,7 @@ import { export function filterByVisibility( data: JsonObject, includeVisibilities: ConfigVisibility[], - visibilityByPath: Map, + visibilityByDataPath: Map, transformFunc?: TransformFunc, withFilteredKeys?: boolean, ): { data: JsonObject; filteredKeys?: string[] } { @@ -40,7 +41,7 @@ export function filterByVisibility( filterPath: string, // Matches the format of the ConfigReader ): JsonValue | undefined { const visibility = - visibilityByPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY; + visibilityByDataPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY; const isVisible = includeVisibilities.includes(visibility); if (typeof jsonVal !== 'object') { @@ -105,3 +106,54 @@ export function filterByVisibility( data: (transform(data, '', '') as JsonObject) ?? {}, }; } + +export function filterErrorsByVisibility( + errors: ValidationError[] | undefined, + includeVisibilities: ConfigVisibility[] | undefined, + visibilityByDataPath: Map, + visibilityBySchemaPath: Map, +): ValidationError[] { + if (!errors) { + return []; + } + if (!includeVisibilities) { + return errors; + } + + const visibleSchemaPaths = Array.from(visibilityBySchemaPath) + .filter(([, v]) => includeVisibilities.includes(v)) + .map(([k]) => k); + + // If we're filtering by visibility we only care about the errors that happened + // in a visible path. + return errors.filter(error => { + // We always include structural errors as we don't know whether there are + // any visible paths within the structures. + if ( + error.keyword === 'type' && + ['object', 'array'].includes(error.params.type) + ) { + return true; + } + + // For fields that were required we use the schema path to determine whether + // it was visible in addition to the data path. This is because the data path + // visibilities are only populated for values that we reached, which we won't + // if the value is missing. + // We don't use this method for all the errors as the data path is more robust + // and doesn't require us to properly trim the schema path. + if (error.keyword === 'required') { + const trimmedPath = error.schemaPath.slice(1, -'/required'.length); + const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`; + if ( + visibleSchemaPaths.some(visiblePath => visiblePath.startsWith(fullPath)) + ) { + return true; + } + } + + const vis = + visibilityByDataPath.get(error.dataPath) ?? DEFAULT_CONFIG_VISIBILITY; + return vis && includeVisibilities.includes(vis); + }); +} diff --git a/packages/config-loader/src/lib/schema/load.test.ts b/packages/config-loader/src/lib/schema/load.test.ts index 73fa55b90c..e2ae3cc2c7 100644 --- a/packages/config-loader/src/lib/schema/load.test.ts +++ b/packages/config-loader/src/lib/schema/load.test.ts @@ -102,4 +102,146 @@ describe('loadConfigSchema', () => { 'Serialized configuration schema is invalid or has an invalid version number', ); }); + + describe('should consider schema', () => { + it('when filtering simple config', async () => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'a', + configSchema: { + type: 'object', + properties: { + key1: { type: 'string', visibility: 'frontend' }, + key2: { type: 'number', visibility: 'secret' }, + }, + }, + }), + }); + + const schema = await loadConfigSchema({ + packagePaths: ['package.json'], + dependencies: [], + }); + + const configs = [ + { data: { key1: 'a', key2: 'not-a-number' }, context: 'test' }, + ]; + + expect(() => schema.process(configs)).toThrow( + 'Config validation failed, Config should be number { type=number } at /key2', + ); + expect(schema.process(configs, { visibility: ['frontend'] })).toEqual([ + { data: { key1: 'a' }, context: 'test' }, + ]); + expect(() => schema.process(configs, { visibility: ['secret'] })).toThrow( + 'Config validation failed, Config should be number { type=number } at /key2', + ); + }); + + it('when filtering nested config', async () => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'a', + configSchema: { + type: 'object', + properties: { + nested: { + allOf: [ + { + type: 'array', + items: { + type: 'object', + visibility: 'frontend', + properties: { + x: { type: 'number' }, + }, + additionalProperties: { + visibility: 'frontend', + type: 'string', + pattern: '^...$', + }, + }, + }, + ], + }, + }, + }, + }), + }); + + const schema = await loadConfigSchema({ + packagePaths: ['package.json'], + dependencies: [], + }); + + const mkConfig = (nested: any) => [ + { data: { nested: [nested] }, context: 'test' }, + ]; + expect( + schema.process(mkConfig({ x: 1 }), { visibility: ['frontend'] }), + ).toEqual([{ data: { nested: [{}] }, context: 'test' }]); + expect(() => schema.process(mkConfig({ y: 1 }))).toThrow( + 'Config validation failed, Config should be string { type=string } at /nested/0/y', + ); + expect(() => + schema.process(mkConfig({ y: 1 }), { visibility: ['frontend'] }), + ).toThrow( + 'Config validation failed, Config should be string { type=string } at /nested/0/y', + ); + expect( + schema.process(mkConfig({ x: 'a' }), { visibility: ['frontend'] }), + ).toEqual([{ data: { nested: [{}] }, context: 'test' }]); + expect( + schema.process(mkConfig({ y: 'aaa' }), { visibility: ['frontend'] }), + ).toEqual([{ data: { nested: [{ y: 'aaa' }] }, context: 'test' }]); + expect(() => + schema.process(mkConfig({ y: 'aaaa' }), { visibility: ['frontend'] }), + ).toThrow( + 'Config validation failed, Config should match pattern "^...$" { pattern=^...$ } at /nested/0/y', + ); + + // This is a bit of an edge case where we have a structural error, these should always be reported + expect(() => + schema.process([{ data: { nested: {} }, context: 'test' }], { + visibility: ['frontend'], + }), + ).toThrow( + 'Config validation failed, Config should be array { type=array } at /nested', + ); + }); + }); + + it('when filtering config with required values', async () => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'a', + configSchema: { + type: 'object', + properties: { + other: { + required: ['x a'], + type: 'object', + properties: { + 'x a': { type: 'number', visibility: 'frontend' }, + }, + }, + }, + }, + }), + }); + + const schema = await loadConfigSchema({ + packagePaths: ['package.json'], + dependencies: [], + }); + + // Errors about required values should also be filtered like the rest + expect(() => + schema.process([{ data: { other: {} }, context: 'test' }], { + visibility: ['frontend'], + }), + ).toThrow( + "Config should have required property 'x a' { missingProperty=x a } at /other", + ); + }); }); diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index 2d4af3454c..2d78785304 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -14,17 +14,23 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { compileConfigSchemas } from './compile'; import { collectConfigSchemas } from './collect'; -import { filterByVisibility } from './filtering'; +import { filterByVisibility, filterErrorsByVisibility } from './filtering'; import { + ValidationError, ConfigSchema, ConfigSchemaPackageEntry, CONFIG_VISIBILITIES, } from './types'; -/** @public */ +/** + * Options that control the loading of configuration schema files in the backend. + * + * @public + */ export type LoadConfigSchemaOptions = | { dependencies: string[]; @@ -34,6 +40,18 @@ export type LoadConfigSchemaOptions = serialized: JsonObject; }; +function errorsToError(errors: ValidationError[]): Error { + const messages = errors.map(({ dataPath, message, params }) => { + const paramStr = Object.entries(params) + .map(([name, value]) => `${name}=${value}`) + .join(' '); + return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; + }); + const error = new Error(`Config validation failed, ${messages.join('; ')}`); + (error as any).messages = messages; + return error; +} + /** * Loads config schema for a Backstage instance. * @@ -67,12 +85,15 @@ export async function loadConfigSchema( { visibility, valueTransform, withFilteredKeys } = {}, ): AppConfig[] { const result = validate(configs); - if (result.errors) { - const error = new Error( - `Config validation failed, ${result.errors.join('; ')}`, - ); - (error as any).messages = result.errors; - throw error; + + const visibleErrors = filterErrorsByVisibility( + result.errors, + visibility, + result.visibilityByDataPath, + result.visibilityBySchemaPath, + ); + if (visibleErrors.length > 0) { + throw errorsToError(visibleErrors); } let processedConfigs = configs; @@ -83,7 +104,7 @@ export async function loadConfigSchema( ...filterByVisibility( data, visibility, - result.visibilityByPath, + result.visibilityByDataPath, valueTransform, withFilteredKeys, ), @@ -94,7 +115,7 @@ export async function loadConfigSchema( ...filterByVisibility( data, Array.from(CONFIG_VISIBILITIES), - result.visibilityByPath, + result.visibilityByDataPath, valueTransform, withFilteredKeys, ), diff --git a/packages/config-loader/src/lib/schema/types.ts b/packages/config-loader/src/lib/schema/types.ts index ef6a197f55..53f4dec88e 100644 --- a/packages/config-loader/src/lib/schema/types.ts +++ b/packages/config-loader/src/lib/schema/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * An sub-set of configuration schema. @@ -50,7 +51,14 @@ export const DEFAULT_CONFIG_VISIBILITY: ConfigVisibility = 'backend'; /** * An explanation of a configuration validation error. */ -type ValidationError = string; +export type ValidationError = { + keyword: string; + dataPath: string; + schemaPath: string; + params: Record; + propertyName?: string; + message?: string; +}; /** * The result of validating configuration data using a schema. @@ -65,7 +73,14 @@ type ValidationResult = { * * The path in the key uses the form `////` */ - visibilityByPath: Map; + visibilityByDataPath: Map; + + /** + * The configuration visibilities that were discovered during validation. + * + * The path in the key uses the form `/properties//items/additionalProperties/` + */ + visibilityBySchemaPath: Map; }; /** diff --git a/packages/config-loader/src/lib/transform/apply.ts b/packages/config-loader/src/lib/transform/apply.ts index 690d280266..80ddad90d0 100644 --- a/packages/config-loader/src/lib/transform/apply.ts +++ b/packages/config-loader/src/lib/transform/apply.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { assertError } from '@backstage/errors'; import { TransformFunc } from './types'; import { isObject } from './utils'; @@ -46,6 +47,7 @@ export async function applyConfigTransforms( break; } } catch (error) { + assertError(error); throw new Error(`error at ${path}, ${error.message}`); } } diff --git a/packages/config-loader/src/lib/transform/include.ts b/packages/config-loader/src/lib/transform/include.ts index 5ea7161e5f..569b61c82c 100644 --- a/packages/config-loader/src/lib/transform/include.ts +++ b/packages/config-loader/src/lib/transform/include.ts @@ -16,7 +16,7 @@ import yaml from 'yaml'; import { extname, dirname, resolve as resolvePath } from 'path'; -import { JsonObject, JsonValue } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/types'; import { isObject } from './utils'; import { TransformFunc, EnvFunc, ReadFileFunc } from './types'; diff --git a/packages/config-loader/src/lib/transform/index.ts b/packages/config-loader/src/lib/transform/index.ts index eaa85cca3c..5053cd7443 100644 --- a/packages/config-loader/src/lib/transform/index.ts +++ b/packages/config-loader/src/lib/transform/index.ts @@ -17,4 +17,3 @@ export { applyConfigTransforms } from './apply'; export { createIncludeTransform } from './include'; export { createSubstitutionTransform } from './substitution'; -export type { EnvFunc } from './types'; diff --git a/packages/config-loader/src/lib/transform/substitution.ts b/packages/config-loader/src/lib/transform/substitution.ts index 21edfb7e57..3c25784dc2 100644 --- a/packages/config-loader/src/lib/transform/substitution.ts +++ b/packages/config-loader/src/lib/transform/substitution.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; import { TransformFunc, EnvFunc } from './types'; /** diff --git a/packages/config-loader/src/lib/transform/types.ts b/packages/config-loader/src/lib/transform/types.ts index afd3550984..0b743cb0fe 100644 --- a/packages/config-loader/src/lib/transform/types.ts +++ b/packages/config-loader/src/lib/transform/types.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; -/** @public */ export type EnvFunc = (name: string) => Promise; export type ReadFileFunc = (path: string) => Promise; diff --git a/packages/config-loader/src/lib/transform/utils.ts b/packages/config-loader/src/lib/transform/utils.ts index a49e9b14bd..b28effa023 100644 --- a/packages/config-loader/src/lib/transform/utils.ts +++ b/packages/config-loader/src/lib/transform/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue, JsonObject } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/types'; export function isObject(obj: JsonValue | undefined): obj is JsonObject { if (typeof obj !== 'object') { diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 315b44c4ee..6803d61786 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -19,15 +19,19 @@ import yaml from 'yaml'; import chokidar from 'chokidar'; import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; import { AppConfig } from '@backstage/config'; +import { ForwardedError } from '@backstage/errors'; import { applyConfigTransforms, readEnvConfig, createIncludeTransform, createSubstitutionTransform, } from './lib'; -import { EnvFunc } from './lib/transform/types'; -/** @public */ +/** + * Options that control the loading of configuration files in the backend. + * + * @public + */ export type LoadConfigOptions = { // The root directory of the config loading context. Used to find default configs. configRoot: string; @@ -43,7 +47,7 @@ export type LoadConfigOptions = { * * @experimental This API is not stable and may change at any point */ - experimentalEnvFunc?: EnvFunc; + experimentalEnvFunc?: (name: string) => Promise; /** * An optional configuration that enables watching of config files. @@ -114,9 +118,7 @@ export async function loadConfig( try { fileConfigs = await loadConfigFiles(); } catch (error) { - throw new Error( - `Failed to read static configuration file, ${error.message}`, - ); + throw new ForwardedError('Failed to read static configuration file', error); } const envConfigs = await readEnvConfig(process.env); diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 9948aa1e29..205db6ef2b 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -3,10 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import type { JsonArray as JsonArray_2 } from '@backstage/types'; +import { JsonObject as JsonObject_2 } from '@backstage/types'; +import type { JsonPrimitive as JsonPrimitive_2 } from '@backstage/types'; +import { JsonValue as JsonValue_2 } from '@backstage/types'; + // @public export type AppConfig = { context: string; - data: JsonObject; + data: JsonObject_2; filteredKeys?: string[]; }; @@ -17,8 +22,8 @@ export type Config = { }; has(key: string): boolean; keys(): string[]; - get(key?: string): T; - getOptional(key?: string): T | undefined; + get(key?: string): T; + getOptional(key?: string): T | undefined; getConfig(key: string): Config; getOptionalConfig(key: string): Config | undefined; getConfigArray(key: string): Config[]; @@ -36,7 +41,7 @@ export type Config = { // @public export class ConfigReader implements Config { constructor( - data: JsonObject | undefined, + data: JsonObject_2 | undefined, context?: string, fallback?: ConfigReader | undefined, prefix?: string, @@ -44,7 +49,7 @@ export class ConfigReader implements Config { // (undocumented) static fromConfigs(configs: AppConfig[]): ConfigReader; // (undocumented) - get(key?: string): T; + get(key?: string): T; // (undocumented) getBoolean(key: string): boolean; // (undocumented) @@ -54,7 +59,7 @@ export class ConfigReader implements Config { // (undocumented) getNumber(key: string): number; // (undocumented) - getOptional(key?: string): T | undefined; + getOptional(key?: string): T | undefined; // (undocumented) getOptionalBoolean(key: string): boolean | undefined; // (undocumented) @@ -77,17 +82,15 @@ export class ConfigReader implements Config { keys(): string[]; } -// @public -export interface JsonArray extends Array {} +// @public @deprecated +export type JsonArray = JsonArray_2; -// @public -export type JsonObject = { - [key in string]?: JsonValue; -}; +// @public @deprecated +export type JsonObject = JsonObject_2; -// @public -export type JsonPrimitive = number | string | boolean | null; +// @public @deprecated +export type JsonPrimitive = JsonPrimitive_2; -// @public -export type JsonValue = JsonObject | JsonArray | JsonPrimitive; +// @public @deprecated +export type JsonValue = JsonValue_2; ``` diff --git a/packages/config/package.json b/packages/config/package.json index ffa2404300..719ab22772 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -30,10 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/types": "^0.1.1", "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils-core": "^0.1.2", + "@backstage/test-utils": "^0.1.19", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/config/src/deprecatedTypes.ts b/packages/config/src/deprecatedTypes.ts new file mode 100644 index 0000000000..823825451b --- /dev/null +++ b/packages/config/src/deprecatedTypes.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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. + */ + +// Temporarily re-export the JSON types from @backstage/types +import type { + JsonArray as CoreJsonArray, + JsonObject as CoreJsonObject, + JsonPrimitive as CoreJsonPrimitive, + JsonValue as CoreJsonValue, +} from '@backstage/types'; + +/** + * A type representing all allowed JSON primitive values. + * + * @public + * @deprecated Please use the same type from `@backstage/types` instead + */ +export type JsonPrimitive = CoreJsonPrimitive; + +/** + * A type representing all allowed JSON object values. + * + * @public + * @deprecated Please use the same type from `@backstage/types` instead + */ +export type JsonObject = CoreJsonObject; + +/** + * A type representing all allowed JSON array values. + * + * @public + * @deprecated Please use the same type from `@backstage/types` instead + */ +export type JsonArray = CoreJsonArray; + +/** + * A type representing all allowed JSON values. + * + * @public + * @deprecated Please use the same type from `@backstage/types` instead + */ +export type JsonValue = CoreJsonValue; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 59b5e8fff8..c8800d0ad2 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -20,12 +20,11 @@ * @packageDocumentation */ -export { ConfigReader } from './reader'; export type { - AppConfig, - Config, JsonArray, JsonObject, JsonPrimitive, JsonValue, -} from './types'; +} from './deprecatedTypes'; +export { ConfigReader } from './reader'; +export type { AppConfig, Config } from './types'; diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 0cea80dca1..3cbab17637 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from '@backstage/test-utils'; import { ConfigReader } from './reader'; const DATA = { diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 92ca0eede3..bc8a860c85 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, Config, JsonValue, JsonObject } from './types'; +import { JsonValue, JsonObject } from '@backstage/types'; +import { AppConfig, Config } from './types'; import cloneDeep from 'lodash/cloneDeep'; import mergeWith from 'lodash/mergeWith'; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index d5f551cd69..a543233277 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -14,33 +14,7 @@ * limitations under the License. */ -/** - * A type representing all allowed JSON primitive values. - * - * @public - */ -export type JsonPrimitive = number | string | boolean | null; - -/** - * A type representing all allowed JSON object values. - * - * @public - */ -export type JsonObject = { [key in string]?: JsonValue }; - -/** - * A type representing all allowed JSON array values. - * - * @public - */ -export interface JsonArray extends Array {} - -/** - * A type representing all allowed JSON values. - * - * @public - */ -export type JsonValue = JsonObject | JsonArray | JsonPrimitive; +import { JsonObject, JsonValue } from '@backstage/types'; /** * A serialized form of configuration data that carries additional context. diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 75490a0548..e01e89f04c 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/core-app-api +## 0.1.18 + +### Patch Changes + +- 202f322927: Atlassian auth provider + + - AtlassianAuth added to core-app-api + - Atlassian provider added to plugin-auth-backend + - Updated user-settings with Atlassian connection + +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/core-components@0.7.1 + - @backstage/core-plugin-api@0.1.11 + +## 0.1.17 + +### Patch Changes + +- 75bc878221: Internal refactor to avoid importing all of `@material-ui/core`. +- Updated dependencies + - @backstage/core-components@0.7.0 + - @backstage/theme@0.2.11 + ## 0.1.16 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 22d5092aa6..03c29daa01 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -15,6 +15,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; +import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { AuthProvider } from '@backstage/core-plugin-api'; import { AuthRequester } from '@backstage/core-plugin-api'; @@ -39,7 +40,7 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; @@ -236,12 +237,25 @@ export class AppThemeSelector implements AppThemeApi { setActiveThemeId(themeId?: string): void; } +// Warning: (ae-missing-release-tag) "AtlassianAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AtlassianAuth { + // Warning: (ae-forgotten-export) The symbol "OAuthApiCreateOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static create({ + discoveryApi, + environment, + provider, + oauthRequestApi, + }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; +} + // Warning: (ae-missing-release-tag) "Auth0Auth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export class Auth0Auth { - // Warning: (ae-forgotten-export) The symbol "OAuthApiCreateOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) static create({ discoveryApi, diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 1381125b23..aee8eb9f02 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.16", + "version": "0.1.18", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.6.1", + "@backstage/core-components": "^0.7.1", "@backstage/config": "^0.1.10", - "@backstage/core-plugin-api": "^0.1.10", - "@backstage/theme": "^0.2.10", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,9 +46,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.15", - "@backstage/test-utils": "^0.1.18", - "@backstage/test-utils-core": "^0.1.3", + "@backstage/cli": "^0.8.0", + "@backstage/test-utils": "^0.1.19", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts index 0ba275ea4d..5e649e2f4c 100644 --- a/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AlertApi, AlertMessage, Observable } from '@backstage/core-plugin-api'; + +import { AlertApi, AlertMessage } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; /** diff --git a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts index 4dceed4749..3dfa43b661 100644 --- a/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts +++ b/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppThemeApi, AppTheme, Observable } from '@backstage/core-plugin-api'; +import { AppThemeApi, AppTheme } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { BehaviorSubject } from '../../../lib/subjects'; const STORAGE_KEY = 'theme'; diff --git a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts index 9cd666e8a3..7fcbde97df 100644 --- a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts +++ b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.test.ts @@ -45,25 +45,25 @@ describe('UrlPatternDiscovery', () => { it('should validate that the pattern is a valid URL', () => { expect(() => { UrlPatternDiscovery.compile('example.com'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: example.com'); + }).toThrow("Invalid discovery URL pattern, URL 'example.com' is invalid"); expect(() => { UrlPatternDiscovery.compile('http://'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: http://'); + }).toThrow("Invalid discovery URL pattern, URL 'http://' is invalid"); expect(() => { UrlPatternDiscovery.compile('abc123'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: abc123'); + }).toThrow("Invalid discovery URL pattern, URL 'abc123' is invalid"); expect(() => { UrlPatternDiscovery.compile('http://example.com:{{pluginId}}'); }).toThrow( - 'Invalid discovery URL pattern, Invalid URL: http://example.com:pluginId', + "Invalid discovery URL pattern, URL 'http://example.com:pluginId' is invalid", ); expect(() => { UrlPatternDiscovery.compile('/{{pluginId}}'); - }).toThrow('Invalid discovery URL pattern, Invalid URL: /pluginId'); + }).toThrow("Invalid discovery URL pattern, URL '/pluginId' is invalid"); expect(() => { UrlPatternDiscovery.compile('http://localhost/{{pluginId}}?forbidden'); diff --git a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts index 81cc5a26b1..58e14c9f04 100644 --- a/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts +++ b/packages/core-app-api/src/apis/implementations/DiscoveryApi/UrlPatternDiscovery.ts @@ -16,6 +16,8 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; +const ERROR_PREFIX = 'Invalid discovery URL pattern,'; + /** * UrlPatternDiscovery is a lightweight DiscoveryApi implementation. * It uses a single template string to construct URLs for each plugin. @@ -30,21 +32,22 @@ export class UrlPatternDiscovery implements DiscoveryApi { */ static compile(pattern: string): UrlPatternDiscovery { const parts = pattern.split(/\{\{\s*pluginId\s*\}\}/); + const urlStr = parts.join('pluginId'); + let url; try { - const urlStr = parts.join('pluginId'); - const url = new URL(urlStr); - if (url.hash) { - throw new Error('URL must not have a hash'); - } - if (url.search) { - throw new Error('URL must not have a query'); - } - if (urlStr.endsWith('/')) { - throw new Error('URL must not end with a slash'); - } - } catch (error) { - throw new Error(`Invalid discovery URL pattern, ${error.message}`); + url = new URL(urlStr); + } catch { + throw new Error(`${ERROR_PREFIX} URL '${urlStr}' is invalid`); + } + if (url.hash) { + throw new Error(`${ERROR_PREFIX} URL must not have a hash`); + } + if (url.search) { + throw new Error(`${ERROR_PREFIX} URL must not have a query`); + } + if (urlStr.endsWith('/')) { + throw new Error(`${ERROR_PREFIX} URL must not end with a slash`); } return new UrlPatternDiscovery(parts); diff --git a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index 9c9a6f20f1..0edbd2f353 100644 --- a/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-app-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, Observable } from '@backstage/core-plugin-api'; + +import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { PublishSubject } from '../../../lib/subjects'; /** diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts index d91cb0ebb0..be643d274b 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { BehaviorSubject } from '../../../lib/subjects'; type RequestQueueEntry = { diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index 6c00cd85cd..1cd25c4b56 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -19,8 +19,8 @@ import { PendingAuthRequest, AuthRequester, AuthRequesterOptions, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests'; import { BehaviorSubject } from '../../../lib/subjects'; diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index d1f712e72a..08c4df7e8c 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { StorageApi, StorageValueChange, ErrorApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; const buckets = new Map(); diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts new file mode 100644 index 0000000000..423c45bc6c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 AtlassianIcon from '@material-ui/icons/AcUnit'; +import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; +import { OAuthApiCreateOptions } from '../types'; + +const DEFAULT_PROVIDER = { + id: 'atlassian', + title: 'Atlassian', + icon: AtlassianIcon, +}; + +class AtlassianAuth { + static create({ + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T { + return OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider, + environment, + }); + } +} + +export default AtlassianAuth; diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/index.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/index.ts new file mode 100644 index 0000000000..fb787be1ce --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { default as AtlassianAuth } from './AtlassianAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 85e3e79ad5..72b4209a34 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -24,8 +24,8 @@ import { ProfileInfo, BackstageIdentity, AuthRequestOptions, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { AuthSessionStore, diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index b622b90b8c..bbc9d23ccc 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -24,3 +24,4 @@ export * from './auth0'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; +export * from './atlassian'; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 24db8d2560..8f685b9266 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -28,8 +28,8 @@ import { SessionState, SessionApi, BackstageIdentityApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; import { OAuthApiCreateOptions } from '../types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index ae4f80c9b1..0a7fb50101 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -25,8 +25,8 @@ import { ProfileInfoApi, BackstageIdentityApi, SessionApi, - Observable, } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SamlSession } from './types'; import { AuthSessionStore, diff --git a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx index d1793f890e..c08674adfe 100644 --- a/packages/core-app-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-app-api/src/apis/system/ApiProvider.test.tsx @@ -25,7 +25,7 @@ import { import { ApiProvider } from './ApiProvider'; import { ApiRegistry } from './ApiRegistry'; import { render } from '@testing-library/react'; -import { withLogCollector } from '@backstage/test-utils-core'; +import { withLogCollector } from '@backstage/test-utils'; import { useVersionedContext } from '@backstage/version-bridge'; describe('ApiProvider', () => { diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index 4e283a1d00..4e3a8487a6 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -15,7 +15,8 @@ */ import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; -import { ThemeProvider, CssBaseline } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api'; import { useObservable } from 'react-use'; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index e9c37a1420..901697edba 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AppConfig, JsonObject } from '@backstage/config'; +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { Button } from '@material-ui/core'; import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/core-app-api/src/app/defaultApis.ts index 5e850f4cce..fb02274cf0 100644 --- a/packages/core-app-api/src/app/defaultApis.ts +++ b/packages/core-app-api/src/app/defaultApis.ts @@ -33,6 +33,7 @@ import { SamlAuth, OneLoginAuth, UnhandledErrorForwarder, + AtlassianAuth, } from '../apis'; import { @@ -55,6 +56,7 @@ import { oneloginAuthApiRef, oidcAuthApiRef, bitbucketAuthApiRef, + atlassianAuthApiRef, } from '@backstage/core-plugin-api'; import OAuth2Icon from '@material-ui/icons/AcUnit'; @@ -244,4 +246,19 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), ]; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts index 919948f531..35b9a808b2 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux'; import { MutableSessionManager, SessionManager } from './types'; diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts index 221a7635ea..80dfa1879e 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import { SessionManager, MutableSessionManager, diff --git a/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts index b70b4bf4ca..4523a868b4 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -15,7 +15,8 @@ */ import { BehaviorSubject } from '../subjects'; -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export class SessionStateTracker { private readonly subject = new BehaviorSubject( diff --git a/packages/core-app-api/src/lib/AuthSessionManager/types.ts b/packages/core-app-api/src/lib/AuthSessionManager/types.ts index 43655b8395..a2a5800974 100644 --- a/packages/core-app-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-app-api/src/lib/AuthSessionManager/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { SessionState } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; export type GetSessionOptions = { optional?: boolean; diff --git a/packages/core-app-api/src/lib/subjects.ts b/packages/core-app-api/src/lib/subjects.ts index e122da819b..79c2c25675 100644 --- a/packages/core-app-api/src/lib/subjects.ts +++ b/packages/core-app-api/src/lib/subjects.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '@backstage/core-plugin-api'; +import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; // TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects. diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx index 12d9e9ee33..fbdd34ae68 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx @@ -83,7 +83,7 @@ const MockRouteSource = (props: { } catch (ex) { return (
- Error at {props.name}: {ex.message} + Error at {props.name}, {String(ex)}
); } @@ -293,12 +293,12 @@ describe('discovery', () => { expect( rendered.getByText( - `Error at outsideWithParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, + `Error at outsideWithParams, Error: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, ), ).toBeInTheDocument(); expect( rendered.getByText( - `Error at outsideNoParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, + `Error at outsideNoParams, Error: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, ), ).toBeInTheDocument(); }); diff --git a/packages/core-components/.eslintrc.js b/packages/core-components/.eslintrc.js index d592a653c8..65a93d3660 100644 --- a/packages/core-components/.eslintrc.js +++ b/packages/core-components/.eslintrc.js @@ -1,8 +1,25 @@ +const base = require('@backstage/cli/config/eslint'); +const [, baseRestrictedImports] = base.rules['no-restricted-imports']; + module.exports = { extends: [require.resolve('@backstage/cli/config/eslint')], rules: { // TODO: add prop types to JS and remove 'react/prop-types': 0, 'jest/expect-expect': 0, + 'no-restricted-imports': [ + 2, + { + ...baseRestrictedImports, + paths: [ + { + // Importing the entire MUI icons packages kills build performance as the list of icons is huge. + name: '@material-ui/core', + message: "Please import '@material-ui/core/...' instead.", + }, + ...baseRestrictedImports.paths, + ], + }, + ], }, }; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index bf3c84c4fd..375f23dfda 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/core-components +## 0.7.1 + +### Patch Changes + +- e535ea766a: Switched to relying on the built-in support for async loading in `react-syntax-highlighter`. This should provide further improvements to async rendering and lazy loading, and avoid test flakiness that was happening because of the significant number or resources being loaded in lazily all at once. +- 2023a9683f: Fixed invalid wrap in DismissableBanner component +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + - @backstage/core-plugin-api@0.1.11 + +## 0.7.0 + +### Minor Changes + +- 5c42360577: Add documentation and more type safety around DependencyGraph +- a20cbf00d2: The syntax highlighting library used by the `CodeSnippet` component is now lazy loaded. This most likely has no effect on existing code, but may break tests as the content of the `CodeSnippet` is now rendered asynchronously. + +### Patch Changes + +- 75bc878221: Internal refactor to avoid importing all of `@material-ui/core`. +- 6ec56d5a57: update the null check to use the optional chaining operator in case of non-null assertion operator is not working in function extractInitials(values: string) +- 81c2a1af86: Resolve a warning in `