diff --git a/.changeset/beige-glasses-dream.md b/.changeset/beige-glasses-dream.md deleted file mode 100644 index c1a9ad1857..0000000000 --- a/.changeset/beige-glasses-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Allow to pass custom TemplateCard to ScaffolderPage diff --git a/.changeset/cuddly-cooks-fry.md b/.changeset/cuddly-cooks-fry.md deleted file mode 100644 index 028b0291bd..0000000000 --- a/.changeset/cuddly-cooks-fry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Added a `useEntityKinds` hook to load a unique list of entity kinds from the catalog. -Fixed a bug in `EntityTypePicker` where the component did not hide when no types were available in returned entities. diff --git a/.changeset/cuddly-impalas-wave.md b/.changeset/cuddly-impalas-wave.md deleted file mode 100644 index 1199ef1482..0000000000 --- a/.changeset/cuddly-impalas-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -GitHub discovery processor passes over repositories that do not have a default branch diff --git a/.changeset/fair-files-rest.md b/.changeset/fair-files-rest.md deleted file mode 100644 index 68d3246aa4..0000000000 --- a/.changeset/fair-files-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -GitHub create repository webhook action: `github:webhook` for Backstage plugin Scaffolder has been added. diff --git a/.changeset/fast-wasps-matter.md b/.changeset/fast-wasps-matter.md new file mode 100644 index 0000000000..6b6a682a37 --- /dev/null +++ b/.changeset/fast-wasps-matter.md @@ -0,0 +1,23 @@ +--- +'@backstage/create-app': patch +--- + +Updated the search configuration class to use the static `fromConfig`-based constructor for the `DefaultCatalogCollator`. + +To apply this change to an existing app, replace the following line in `search.ts`: + +```diff +-collator: new DefaultCatalogCollator({ discovery }) ++collator: DefaultCatalogCollator.fromConfig(config, { discovery }) +``` + +The `config` parameter was not needed before, so make sure you also add that in the signature of `createPlugin` +in `search.ts`: + +```diff +export default async function createPlugin({ + logger, + discovery, ++ config, +}: PluginEnvironment) { +``` diff --git a/.changeset/forty-terms-join.md b/.changeset/forty-terms-join.md deleted file mode 100644 index 1476ebff9c..0000000000 --- a/.changeset/forty-terms-join.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/techdocs-common': patch ---- - -Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. - -Adding extra information to indexed tech docs documents for search. diff --git a/.changeset/good-papayas-fetch.md b/.changeset/good-papayas-fetch.md deleted file mode 100644 index 6cd74ce064..0000000000 --- a/.changeset/good-papayas-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Adds optional namespacing for users in the GitHub Multi Org Plugin diff --git a/.changeset/happy-cups-tap.md b/.changeset/happy-cups-tap.md new file mode 100644 index 0000000000..051f9def1c --- /dev/null +++ b/.changeset/happy-cups-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Allow the catalog search collator to filter the entities that it indexes diff --git a/.changeset/hip-beers-attend.md b/.changeset/hip-beers-attend.md deleted file mode 100644 index 802bfe7397..0000000000 --- a/.changeset/hip-beers-attend.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-shortcuts': patch ---- - -Bumped `react-hook-form` to `^7.12.2` diff --git a/.changeset/honest-suits-approve.md b/.changeset/honest-suits-approve.md deleted file mode 100644 index 59cdb09762..0000000000 --- a/.changeset/honest-suits-approve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-git-release-manager': patch ---- - -Wrap each feature in custom feature arrays with an element containing a 'key' to avoid missing-key-warnings diff --git a/.changeset/nasty-ads-nail.md b/.changeset/nasty-ads-nail.md deleted file mode 100644 index 0518b1e569..0000000000 --- a/.changeset/nasty-ads-nail.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/core-components': minor ---- - -Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: - -```diff -- -+}> -``` diff --git a/.changeset/nasty-jokes-camp.md b/.changeset/nasty-jokes-camp.md deleted file mode 100644 index e30ff75911..0000000000 --- a/.changeset/nasty-jokes-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Don't open a new tab when clicking on the ownership card. diff --git a/.changeset/pretty-cameras-give.md b/.changeset/pretty-cameras-give.md deleted file mode 100644 index 8f9f4a83fa..0000000000 --- a/.changeset/pretty-cameras-give.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Update the `AboutCard` to properly support non-standard entity types and rework the defaults for the build-in kinds. - -This change also uses `useElementFilter(...)` instead of `React.children.count(...)` in `AboutField` to properly recognize whether children are available. diff --git a/.changeset/serious-maps-try.md b/.changeset/serious-maps-try.md deleted file mode 100644 index 78e953dfd6..0000000000 --- a/.changeset/serious-maps-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -typo fix `tenentId` in Azure auth provider docs diff --git a/.changeset/short-years-smile.md b/.changeset/short-years-smile.md deleted file mode 100644 index 6e7b882c1e..0000000000 --- a/.changeset/short-years-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Added the ability to switch entity kind on the catalog index page. This is a non-breaking change, but if you created a custom `CatalogPage` and wish to use this feature, make the modifications shown on [#6895](https://github.com/backstage/backstage/pull/6895). diff --git a/.changeset/sixty-cougars-study.md b/.changeset/sixty-cougars-study.md deleted file mode 100644 index 80db2913d0..0000000000 --- a/.changeset/sixty-cougars-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-shortcuts': patch ---- - -Fix material-ui warning about deprecated `onExit` prop diff --git a/.changeset/strange-zoos-grab.md b/.changeset/strange-zoos-grab.md deleted file mode 100644 index 78a6c6eeee..0000000000 --- a/.changeset/strange-zoos-grab.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Add an optional `metadata.title` field to all entity kinds. - -This used to be available on only the `Template` kind, and we have decided that the metadata block should be the same for all kinds. A title can be useful especially in large and complex catalogs where users have a tough time navigating or discerning among the entities. - -It also carries some risk. You do not want to end up giving a title that collides with an actual name, which at best leads to confusion and at worst could be a liability. We do not perform any collision detection in the catalog. If you want to disallow this facility you may want to add a small processor that makes sure it's not set. - -At the time of writing this message, only the scaffolder actually makes use of this field for display purposes. diff --git a/.changeset/strong-beers-ring.md b/.changeset/strong-beers-ring.md deleted file mode 100644 index 17a6d89fcc..0000000000 --- a/.changeset/strong-beers-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Minor tweaks to the create-app template to match earlier documented changes diff --git a/.changeset/stupid-ants-call.md b/.changeset/stupid-ants-call.md deleted file mode 100644 index b2dc16d0e2..0000000000 --- a/.changeset/stupid-ants-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Export type that are needed to implement a new `UrlReader` diff --git a/.changeset/sweet-jars-travel.md b/.changeset/sweet-jars-travel.md deleted file mode 100644 index 83fcd13623..0000000000 --- a/.changeset/sweet-jars-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Update README to show a simpler proxy configuration. diff --git a/.changeset/techdocs-trains-beg.md b/.changeset/techdocs-trains-beg.md deleted file mode 100644 index 05161b7093..0000000000 --- a/.changeset/techdocs-trains-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Force using `posix` path for cloud storage diff --git a/.changeset/thick-trees-pay.md b/.changeset/thick-trees-pay.md deleted file mode 100644 index 6ee7b514ca..0000000000 --- a/.changeset/thick-trees-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Use a `Link` for the edit button on the `AboutCard` instead of doing `window.open(...)` diff --git a/.changeset/wicked-singers-push.md b/.changeset/wicked-singers-push.md deleted file mode 100644 index 3c34a959ed..0000000000 --- a/.changeset/wicked-singers-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Fix OwnershipCard links to support namespaced owners diff --git a/app-config.yaml b/app-config.yaml index 9f365becef..c5c4a23bdd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -133,6 +133,9 @@ kafka: brokers: - localhost:9092 +allure: + baseUrl: http://localhost:5050/allure-docker-service + integrations: github: - host: github.com diff --git a/docs/assets/features/catalog/life-of-an-entity_ingestion.svg b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg new file mode 100644 index 0000000000..b250744fcd --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
change events
change events
Entity Provider
Entity Provider
change events
change events
Entity Provider
Entity Provider
External
Source
External...
External
Source
External...
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_overview.svg b/docs/assets/features/catalog/life-of-an-entity_overview.svg new file mode 100644 index 0000000000..3fc35b4775 --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_overview.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
Processed Entities
Processed Entities
Relations
Relations
Edges
Edges
Final Entities
Final Entities
Search
Search
Errors
Errors
Processors
Processors
Stitcher
Stitcher
Entity Providers
Entity Providers
External
Sources
External...
Catalog
API
Catalog...
Processing
Processing
Stitching
Stitching
Ingestion
Ingestion
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 8b07e1a51a..7a9aa099e9 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -547,8 +547,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a component is the singular entity (commonly a team) that bears ultimate responsibility for the component, and has the authority and @@ -566,8 +566,8 @@ component, but there will always be one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the component -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +component belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -575,8 +575,8 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.subcomponentOf` [optional] -An [entity reference](#string-references) to another component of which the -component is a part, e.g. `spotify-ios-app`. This field is optional. +An [entity reference](references.md#string-references) to another component of +which the component is a part, e.g. `spotify-ios-app`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -584,8 +584,8 @@ component is a part, e.g. `spotify-ios-app`. This field is optional. ### `spec.providesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -provided by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are provided by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -593,8 +593,8 @@ provided by the component, e.g. `artist-api`. This field is optional. ### `spec.consumesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -consumed by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are consumed by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -602,9 +602,9 @@ consumed by the component, e.g. `artist-api`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the component depends on, e.g. `artists-db`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the component depends on, e.g. `artists-db`. This +field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -732,8 +732,8 @@ You can find out more about the `steps` key ### `spec.owner` [optional] -An [entity reference](#string-references) to the owner of the template, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +template, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a Template is the singular entity (commonly a team) that bears ultimate responsibility for the Template, and has the authority and @@ -842,8 +842,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of an API is the singular entity (commonly a team) that bears ultimate responsibility for the API, and has the authority and capability @@ -861,8 +861,8 @@ one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the API belongs to, -e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +API belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1069,8 +1069,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Resource`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the resource, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +resource, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a resource is the singular entity (commonly a team) that bears ultimate responsibility for the resource, and has the authority and @@ -1101,8 +1101,8 @@ Some common values for this field could be: ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the resource -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +resource belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1110,9 +1110,9 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the resource depends on, e.g. `artist-lookup`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the resource depends on, e.g. `artist-lookup`. +This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -1156,8 +1156,8 @@ Exactly equal to `backstage.io/v1alpha1` and `System`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the system, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +system, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a system is the singular entity (commonly a team) that bears ultimate responsibility for the system, and has the authority and @@ -1175,8 +1175,8 @@ but there will always be one ultimate owner. ### `spec.domain` [optional] -An [entity reference](#string-references) to the domain that the system belongs -to, e.g. `artists`. This field is optional. +An [entity reference](references.md#string-references) to the domain that the +system belongs to, e.g. `artists`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1215,8 +1215,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Domain`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the domain, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +domain, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a domain is the singular entity (commonly a team) that bears ultimate responsibility for the domain, and has the authority and diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md new file mode 100644 index 0000000000..817529c0f2 --- /dev/null +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -0,0 +1,130 @@ +--- +id: life-of-an-entity +title: The Life of an Entity +sidebar_label: The Life of an Entity +# prettier-ignore +description: The life cycle of entities, from being introduced into the catalog, through processing, to being removed again +--- + +This document gives a high level overview of the catalog backend, and the +technical processes involved in making entities flow through it. It is mainly +aimed at developers who want to understand the internals while installing or +extending the catalog. However, it can be informative for other personas too. + +## Key Concepts + +The catalog forms a hub of sorts, where entities are ingested from various +authoritative sources and held in a database, subject to automated processing, +and then presented through an API for quick and easy access by Backstage and +others. The most common source is [YAML files](descriptor-format.md) on a +standard format, living in version control systems near the source code of +systems that they describe. Those files are registered with the catalog and +maintained by the respective owners. The catalog makes sure to keep itself up to +date with changes to those files. + +The main extension points where developers can customize the catalog are: + +- _Entity providers_, that feed initial raw entity data into the catalog, +- _Policies_, that establish baseline rules about the shape of entities, +- _Processors_, that validate, analyze, and mutate the raw entity data into its + final form. + +The high level processes involved are: + +- _Ingestion_, where entity providers fetch raw entity data from external + sources and seed it into the database, +- _Processing_, where the policies and processors continually treat the ingested + data and may emit both other raw entities (that are also subject to + processing), errors, relations to other entities, etc., +- _Stitching_, where all of the data emitted by various processors are assembled + together into the final output entity. + +An entity is not visible to the outside world (through the catalog API), until +it has passed through the last process and landed among the final entities. + +![General overview](../../assets/features/catalog/life-of-an-entity_overview.svg) + +The details of these processes are described below. + +## Ingestion + +Each catalog deployment has a number of entity providers installed. They are +responsible for fetching data from external authoritative sources in any way +that they see fit, to translate those into entity objects, and to notify the +database when those entities are added or removed. These are the _unprocessed +entities_ that will be subject to later processing (see below), and they form +the very basis of existence for entities. If there were no entity providers, no +entities would ever enter the system. + +The database always keeps track of the set of entities that belong to each +provider; no two providers can try to output the same entity. And when a +provider signals the removal of an entity, then that leads to an _eager +deletion_: the entity and all auxiliary data that it has led to in the database +is immediately purged. + +![Ingestion overview](../../assets/features/catalog/life-of-an-entity_ingestion.svg) + +There are two providers installed by default: the one that deals with user +registered locations (e.g. URLs to YAML files), and the one that deals with +static locations in the app-config. You can add more third party providers by +passing them to the catalog builder in your backend initialization code, and you +can easily write your own. + +An entity provider is a class that implements the `EntityProvider` interface. It +has three main parts: + +- The identity: Each provider instance has a unique, stable identifier that the + database can use to keep track of the originator of each unprocessed entity. +- The connection: During backend startup, each provider is attached to the + catalog runtime. +- The stream of events: During its lifetime, the provider can issue change + events to the runtime at any point in time, to modify its set of unprocessed + entities. + +It is entirely up to the provider to choose how and when it produces these +change events. For example, the app-config provider only fires off an update at +startup and then lies dormant. The location database provider does an initial +update at startup, and then small delta updates every time a location database +change is detected. The LDAP provider is driven externally by a timer loop that +occasionally triggers a full update. Some future provider may be entirely event +driven, feeding off an event bus or web hook. There is no magic coordination +among providers; if they need to arrange synchronization or locking among +themselves for example to avoid duplicate work across multiple catalog service +machines, they need to handle that out-of-band. + +The entities that are emitted get some coarse validation applied to them, to +ensure that they at least adhere to the most basic schema rules about how an +entity should be shaped. For example, they need to have a `kind`, a +`metadata.name`, and optionally a `metadata.namespace`, among others. Apart from +that, the ingestion stage considers its work done, and stores the unprocessed +entities to be picked up at a later time by the processing system. This means +that the more precise validation rules that you put in place on entities are +_not_ yet applied at this stage. + +## Processing + +Every unprocessed entity comes with a timestamp, which tells at what time that +the processing loop should next try to process it. When the entity first +appears, this timestamp is set to "now" - asking for it to be picked up as soon +as possible. + +Each catalog deployment has a number of processors installed. They are +responsible for receiving unprocessed entities that the catalog decided are due +for processing, and then running that data through a number of processing +stages. mutating the entity and emitting auxiliary data about it. When all of +that is done, the catalog takes all of that information and stores it as the +processed entity, and errors and relations to other entities separately. Then, +the catalog checks to see what entities are touched by that output, and triggers +the final assembly of those (see Stitching below). + +There are several stages involved in the processing. + +> TODO: More info here + +## Stitching + +The stitching is currently a fixed process, that cannot be modified or extended. +This means that any modifications you want to make on the final result, has to +happen during ingestion or processing. + +> TODO: More info here diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 5fa14472d1..326ac02969 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -148,12 +148,18 @@ const AppRoutes = () => { > an external storage provider and external CI/CD is used). If you use the > "basic" or "out-of-the-box" setup, you can stop here! No action needed. -The beta version of TechDocs (v0.x.y) made a breaking change to the way TechDocs -content was accessed and stored, allowing pages to be accessed with -case-insensitive entity triplet paths (e.g. `/docs/namespace/kind/name` whereas -in prior versions, they could only be accessed at `/docs/namespace/Kind/name`). -In order to enable this change, documentation has to be stored in an external -storage provider using an object key whose entity triplet is lower-cased. +For the purposes of this guide, TechDocs Beta version is defined as: + +- **TechDocs Plugin**: At least `v0.11.0` +- **TechDocs Backend Plugin**: At least `v0.10.0` +- **TechDocs CLI**: At least `v0.7.0` + +The beta version of TechDocs made a breaking change to the way TechDocs content +was accessed and stored, allowing pages to be accessed with case-insensitive +entity triplet paths (e.g. `/docs/namespace/kind/name` whereas in prior +versions, they could only be accessed at `/docs/namespace/Kind/name`). In order +to enable this change, documentation has to be stored in an external storage +provider using an object key whose entity triplet is lower-cased. New installations of TechDocs since the beta version will work fine with no action, but for those who were running TechDocs prior to this version, a diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 7973a54ab6..862c3c0cb6 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -128,7 +128,7 @@ const themeOptions = createThemeOptions({ tool: genPageTheme(['#123456','#123456'], shapes.round), service: genPageTheme(['#123456','#123456'], shapes.wave), website: genPageTheme(['#123456','#123456'], shapes.wave), - library: genPageTheme(['#123456','#123456'] shapes.wave), + library: genPageTheme(['#123456','#123456'], shapes.wave), other: genPageTheme(['#123456','#123456'], shapes.wave), app: genPageTheme(['#123456','#123456'], shapes.wave), apis: genPageTheme(['#123456','#123456'], shapes.wave), diff --git a/microsite/data/plugins/allure.yaml b/microsite/data/plugins/allure.yaml new file mode 100644 index 0000000000..defa7158c0 --- /dev/null +++ b/microsite/data/plugins/allure.yaml @@ -0,0 +1,9 @@ +--- +title: Allure Reports +author: Deepak Bhardwaj +authorUrl: https://github.com/deepak-bhardwaj-ps +category: Reporting +description: View Allure reports for your components in Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/allure +iconUrl: https://avatars.githubusercontent.com/u/5879127 +npmPackageName: '@backstage/plugin-allure' diff --git a/microsite/data/plugins/home.yaml b/microsite/data/plugins/home.yaml new file mode 100644 index 0000000000..e37b8c5e03 --- /dev/null +++ b/microsite/data/plugins/home.yaml @@ -0,0 +1,9 @@ +--- +title: Home +author: Spotify +authorUrl: https://github.com/spotify +category: Discovery +description: This plugin provides a composable home page, and ability to create home page components +documentation: https://github.com/backstage/backstage/blob/master/plugins/home/README.md +iconUrl: img/home.png +npmPackageName: '@backstage/plugin-home' diff --git a/microsite/static/img/home.png b/microsite/static/img/home.png new file mode 100644 index 0000000000..e217581586 Binary files /dev/null and b/microsite/static/img/home.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 6840fee68a..41e4d16c3f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' + - The Life of an Entity: 'features/software-catalog/life-of-an-entity.md' - Catalog Configuration: 'features/software-catalog/configuration.md' - System Model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index a3af287fc8..7de19802e3 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,47 @@ # example-app +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.11.2 + - @backstage/cli@0.7.10 + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/plugin-techdocs@0.11.1 + - @backstage/core-components@0.4.0 + - @backstage/plugin-catalog-import@0.5.20 + - @backstage/plugin-shortcuts@0.1.7 + - @backstage/plugin-org@0.3.21 + - @backstage/plugin-catalog@0.6.14 + - @backstage/search-common@0.2.0 + - @backstage/plugin-search@0.4.9 + - @backstage/catalog-model@0.9.1 + - @backstage/core-app-api@0.1.11 + - @backstage/integration-react@0.1.8 + - @backstage/plugin-api-docs@0.6.7 + - @backstage/plugin-badges@0.2.8 + - @backstage/plugin-circleci@0.2.22 + - @backstage/plugin-cloudbuild@0.2.22 + - @backstage/plugin-code-coverage@0.1.10 + - @backstage/plugin-cost-insights@0.11.5 + - @backstage/plugin-explore@0.3.15 + - @backstage/plugin-gcp-projects@0.3.3 + - @backstage/plugin-github-actions@0.4.17 + - @backstage/plugin-graphiql@0.2.15 + - @backstage/plugin-home@0.2.1 + - @backstage/plugin-jenkins@0.5.5 + - @backstage/plugin-kafka@0.2.14 + - @backstage/plugin-kubernetes@0.4.12 + - @backstage/plugin-lighthouse@0.2.24 + - @backstage/plugin-newrelic@0.3.3 + - @backstage/plugin-pagerduty@0.3.12 + - @backstage/plugin-rollbar@0.3.13 + - @backstage/plugin-sentry@0.3.20 + - @backstage/plugin-tech-radar@0.4.6 + - @backstage/plugin-todo@0.1.8 + - @backstage/plugin-user-settings@0.3.4 + ## 0.2.43 ### Patch Changes diff --git a/packages/app/cypress/integration/components/search/SearchPage.js b/packages/app/cypress/integration/components/search/SearchPage.js index 4db7acf4fd..6a18d16198 100644 --- a/packages/app/cypress/integration/components/search/SearchPage.js +++ b/packages/app/cypress/integration/components/search/SearchPage.js @@ -34,7 +34,7 @@ describe('SearchPage', () => { cy.visit('/search-next', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=`) .resolves({ ok: true, json: () => ({ results }), @@ -56,7 +56,7 @@ describe('SearchPage', () => { onBeforeLoad(win) { cy.stub(win, 'fetch') .withArgs( - `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental&pageCursor=`, + `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental`, ) .resolves({ ok: true, @@ -102,7 +102,7 @@ describe('SearchPage', () => { cy.visit('/search-next?query=backstage', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=backstage&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=backstage`) .resolves({ ok: true, json: () => ({ results: [] }), diff --git a/packages/app/package.json b/packages/app/package.json index 162474fd0e..011fcb59af 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,46 +1,46 @@ { "name": "example-app", - "version": "0.2.43", + "version": "0.2.44", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.9", - "@backstage/core-app-api": "^0.1.10", - "@backstage/core-components": "^0.3.3", + "@backstage/catalog-model": "^0.9.1", + "@backstage/cli": "^0.7.10", + "@backstage/core-app-api": "^0.1.11", + "@backstage/core-components": "^0.4.0", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-api-docs": "^0.6.6", - "@backstage/plugin-badges": "^0.2.7", - "@backstage/plugin-catalog": "^0.6.13", - "@backstage/plugin-catalog-import": "^0.5.19", - "@backstage/plugin-catalog-react": "^0.4.4", - "@backstage/plugin-circleci": "^0.2.21", - "@backstage/plugin-cloudbuild": "^0.2.21", - "@backstage/plugin-code-coverage": "^0.1.9", - "@backstage/plugin-cost-insights": "^0.11.4", - "@backstage/plugin-explore": "^0.3.14", - "@backstage/plugin-gcp-projects": "^0.3.2", - "@backstage/plugin-github-actions": "^0.4.16", - "@backstage/plugin-graphiql": "^0.2.14", - "@backstage/plugin-home": "^0.2.0", - "@backstage/plugin-jenkins": "^0.5.4", - "@backstage/plugin-kafka": "^0.2.13", - "@backstage/plugin-kubernetes": "^0.4.11", - "@backstage/plugin-lighthouse": "^0.2.23", - "@backstage/plugin-newrelic": "^0.3.2", - "@backstage/plugin-org": "^0.3.20", - "@backstage/plugin-pagerduty": "0.3.11", - "@backstage/plugin-rollbar": "^0.3.12", - "@backstage/plugin-scaffolder": "^0.11.1", - "@backstage/plugin-search": "^0.4.8", - "@backstage/plugin-sentry": "^0.3.19", - "@backstage/plugin-shortcuts": "^0.1.6", - "@backstage/plugin-tech-radar": "^0.4.5", - "@backstage/plugin-techdocs": "^0.11.0", - "@backstage/plugin-todo": "^0.1.7", - "@backstage/plugin-user-settings": "^0.3.3", - "@backstage/search-common": "^0.1.3", + "@backstage/integration-react": "^0.1.8", + "@backstage/plugin-api-docs": "^0.6.7", + "@backstage/plugin-badges": "^0.2.8", + "@backstage/plugin-catalog": "^0.6.14", + "@backstage/plugin-catalog-import": "^0.5.20", + "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/plugin-circleci": "^0.2.22", + "@backstage/plugin-cloudbuild": "^0.2.22", + "@backstage/plugin-code-coverage": "^0.1.10", + "@backstage/plugin-cost-insights": "^0.11.5", + "@backstage/plugin-explore": "^0.3.15", + "@backstage/plugin-gcp-projects": "^0.3.3", + "@backstage/plugin-github-actions": "^0.4.17", + "@backstage/plugin-graphiql": "^0.2.15", + "@backstage/plugin-home": "^0.2.1", + "@backstage/plugin-jenkins": "^0.5.5", + "@backstage/plugin-kafka": "^0.2.14", + "@backstage/plugin-kubernetes": "^0.4.12", + "@backstage/plugin-lighthouse": "^0.2.24", + "@backstage/plugin-newrelic": "^0.3.3", + "@backstage/plugin-org": "^0.3.21", + "@backstage/plugin-pagerduty": "0.3.12", + "@backstage/plugin-rollbar": "^0.3.13", + "@backstage/plugin-scaffolder": "^0.11.2", + "@backstage/plugin-search": "^0.4.9", + "@backstage/plugin-sentry": "^0.3.20", + "@backstage/plugin-shortcuts": "^0.1.7", + "@backstage/plugin-tech-radar": "^0.4.6", + "@backstage/plugin-techdocs": "^0.11.1", + "@backstage/plugin-todo": "^0.1.8", + "@backstage/plugin-user-settings": "^0.3.4", + "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index cf4e610b80..54f59d1a6e 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; -import Pagination from '@material-ui/lab/Pagination'; +import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; import { + DefaultResultListItem, SearchBar, SearchFilter, SearchResult, + SearchResultPager, SearchType, - DefaultResultListItem, } from '@backstage/plugin-search'; -import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; -import { SearchResultSet } from '@backstage/search-common'; +import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; +import React from 'react'; const useStyles = makeStyles((theme: Theme) => ({ bar: { @@ -43,57 +42,6 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -// TODO: Move this into the search plugin once pagination is natively supported. -// See: https://github.com/backstage/backstage/issues/6062 -const SearchResultList = ({ results }: SearchResultSet) => { - const pageSize = 10; - const [page, setPage] = useState(1); - const changePage = (_: any, pageIndex: number) => { - setPage(pageIndex); - }; - const pageAmount = Math.ceil((results.length || 0) / pageSize); - return ( - <> - - {results - .slice(pageSize * (page - 1), pageSize * page) - .map(({ type, document }) => { - switch (type) { - case 'software-catalog': - return ( - - ); - case 'techdocs': - return ( - - ); - default: - return ( - - ); - } - })} - - - - ); -}; - const SearchPage = () => { const classes = useStyles(); return ( @@ -127,8 +75,37 @@ const SearchPage = () => { - {({ results }) => } + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'software-catalog': + return ( + + ); + case 'techdocs': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f3fcd5a855..dc83e3aa3c 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-common +## 0.9.1 + +### Patch Changes + +- 714a2a918: Export type that are needed to implement a new `UrlReader` +- Updated dependencies + - @backstage/integration@0.6.3 + ## 0.9.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 15a559f960..87d1053165 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.0", + "version": "0.9.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -33,7 +33,7 @@ "@backstage/config": "^0.1.8", "@backstage/config-loader": "^0.6.7", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.2", + "@backstage/integration": "^0.6.3", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.10", "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index b27dbd07e7..9ca401a105 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,23 @@ # example-backend +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.13.6 + - @backstage/plugin-scaffolder-backend@0.15.3 + - @backstage/plugin-techdocs-backend@0.10.1 + - @backstage/plugin-auth-backend@0.3.24 + - @backstage/integration@0.6.3 + - @backstage/plugin-search-backend@0.2.6 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.3 + - @backstage/plugin-search-backend-module-pg@0.2.0 + - @backstage/plugin-search-backend-node@0.4.2 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + - example-app@0.2.44 + ## 0.2.43 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 763d891f77..56b73e0591 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.43", + "version": "0.2.44", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,15 +27,15 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", + "@backstage/backend-common": "^0.9.1", "@backstage/catalog-client": "^0.3.17", - "@backstage/catalog-model": "^0.9.0", + "@backstage/catalog-model": "^0.9.1", "@backstage/config": "^0.1.8", - "@backstage/integration": "^0.6.2", + "@backstage/integration": "^0.6.3", "@backstage/plugin-app-backend": "^0.3.16", - "@backstage/plugin-auth-backend": "^0.3.23", + "@backstage/plugin-auth-backend": "^0.3.24", "@backstage/plugin-badges-backend": "^0.1.9", - "@backstage/plugin-catalog-backend": "^0.13.5", + "@backstage/plugin-catalog-backend": "^0.13.6", "@backstage/plugin-code-coverage-backend": "^0.1.10", "@backstage/plugin-graphql-backend": "^0.1.9", "@backstage/plugin-jenkins-backend": "^0.1.4", @@ -43,19 +43,19 @@ "@backstage/plugin-kafka-backend": "^0.2.9", "@backstage/plugin-proxy-backend": "^0.2.12", "@backstage/plugin-rollbar-backend": "^0.1.14", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/plugin-scaffolder-backend": "^0.15.3", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", - "@backstage/plugin-search-backend": "^0.2.5", - "@backstage/plugin-search-backend-node": "^0.4.0", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.2", - "@backstage/plugin-search-backend-module-pg": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.10.0", + "@backstage/plugin-search-backend": "^0.2.6", + "@backstage/plugin-search-backend-node": "^0.4.2", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.3", + "@backstage/plugin-search-backend-module-pg": "^0.2.0", + "@backstage/plugin-techdocs-backend": "^0.10.1", "@backstage/plugin-todo-backend": "^0.1.11", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", - "azure-devops-node-api": "^10.2.2", + "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.43", + "example-app": "^0.2.44", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -67,7 +67,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.10", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 3470110408..2da108608e 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -68,7 +68,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), }); indexBuilder.addCollator({ diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 5dbbfd241f..3559c661ef 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/catalog-model +## 0.9.1 + +### Patch Changes + +- 13dc3735c: Add an optional `metadata.title` field to all entity kinds. + + This used to be available on only the `Template` kind, and we have decided that the metadata block should be the same for all kinds. A title can be useful especially in large and complex catalogs where users have a tough time navigating or discerning among the entities. + + It also carries some risk. You do not want to end up giving a title that collides with an actual name, which at best leads to confusion and at worst could be a liability. We do not perform any collision detection in the catalog. If you want to disallow this facility you may want to add a small processor that makes sure it's not set. + + At the time of writing this message, only the scaffolder actually makes use of this field for display purposes. + ## 0.9.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 13c3214252..5a16a1d310 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.9.0", + "version": "0.9.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.2", + "@backstage/cli": "^0.7.10", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 064438d17e..47f4545ea2 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/cli +## 0.7.10 + +### Patch Changes + +- 5e803edb8: Added support for importing font files. Imports in CSS via `url()` are supported for the final frontend bundle, but not for packages that are built for publishing. Module imports of fonts files from TypeScript are supported everywhere. +- b5118ff76: Updated dependencies + ## 0.7.9 ### Patch Changes diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 879e9b0b05..0bd0922004 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -68,6 +68,26 @@ declare module '*.svg' { export default src; } +declare module '*.eot' { + const src: string; + export default src; +} + +declare module '*.woff' { + const src: string; + export default src; +} + +declare module '*.woff2' { + const src: string; + export default src; +} + +declare module '*.ttf' { + const src: string; + export default src; +} + declare module '*.css' { const classes: { readonly [key: string]: string }; export default classes; diff --git a/packages/cli/package.json b/packages/cli/package.json index 13d146de72..831a4af0c3 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.9", + "version": "0.7.10", "private": false, "publishConfig": { "access": "public" @@ -51,7 +51,7 @@ "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.5.x", "@types/webpack-env": "^1.15.2", - "@typescript-eslint/eslint-plugin": "^v4.28.3", + "@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", @@ -120,12 +120,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.0", + "@backstage/backend-common": "^0.9.1", "@backstage/config": "^0.1.8", - "@backstage/core-components": "^0.3.3", + "@backstage/core-components": "^0.4.0", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/core-app-api": "^0.1.10", - "@backstage/dev-utils": "^0.2.7", + "@backstage/core-app-api": "^0.1.11", + "@backstage/dev-utils": "^0.2.8", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@types/diff": "^5.0.0", diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 60a5f0efca..3c95a87bdd 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -80,7 +80,17 @@ export const makeConfigs = async ( postcss(), forwardFileImports({ exclude: /\.icon\.svg$/, - include: [/\.svg$/, /\.png$/, /\.gif$/, /\.jpg$/, /\.jpeg$/], + include: [ + /\.svg$/, + /\.png$/, + /\.gif$/, + /\.jpg$/, + /\.jpeg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], }), json(), yaml(), diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 34a441139c..b0a9b25466 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -89,6 +89,17 @@ export const transforms = (options: TransformOptions): Transforms => { name: 'static/[name].[hash:8].[ext]', }, }, + { + test: /\.(eot|woff|woff2|ttf)$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: 'static/[name].[hash:8].[ext]', + }, + }, + ], + }, { test: /\.ya?ml$/, use: require.resolve('yml-loader'), diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 74a3022bf5..e0b3bfb693 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/codemods +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + - @backstage/core-app-api@0.1.11 + ## 0.1.11 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 04bf0ac014..b14d3e880e 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.11", + "version": "0.1.12", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index db5158a697..55b20aac8b 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-app-api +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + ## 0.1.10 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 90c962be3e..e0801b45db 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.10", + "version": "0.1.11", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.3", + "@backstage/core-components": "^0.4.0", "@backstage/config": "^0.1.8", "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.10", @@ -44,7 +44,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.10", "@backstage/test-utils": "^0.1.17", "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 1b56b033b5..0f89c26682 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/core-components +## 0.4.0 + +### Minor Changes + +- 3ed78fca3: Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: + + ```diff + - + +}> + ``` + +### Patch Changes + +- e0a6aea82: Bumped `react-hook-form` to `^7.12.2` + ## 0.3.3 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index ae6daece4d..720482f46b 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.3.3", + "version": "0.4.0", "private": false, "publishConfig": { "access": "public", @@ -70,8 +70,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.10", - "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.11", + "@backstage/cli": "^0.7.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 9ad697b74d..670613dac9 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.3.39 + +### Patch Changes + +- 25924638b: Minor tweaks to the create-app template to match earlier documented changes + ## 0.3.38 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 31ac1ba271..f0939c213e 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.38", + "version": "0.3.39", "private": false, "publishConfig": { "access": "public" diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts index 248ed37ea8..7fc317d23d 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts @@ -10,6 +10,7 @@ import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; export default async function createPlugin({ logger, discovery, + config, }: PluginEnvironment) { // Initialize a connection to a search engine. const searchEngine = new LunrSearchEngine({ logger }); @@ -19,7 +20,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), }); // The scheduler controls when documents are gathered from collators and sent diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index a05e39a69b..f0c4c062b4 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/dev-utils +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/core-components@0.4.0 + - @backstage/catalog-model@0.9.1 + - @backstage/core-app-api@0.1.11 + - @backstage/integration-react@0.1.8 + ## 0.2.7 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3990296f4c..9317c0a107 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.7", + "version": "0.2.8", "private": false, "publishConfig": { "access": "public", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.9", - "@backstage/core-components": "^0.3.2", + "@backstage/core-app-api": "^0.1.11", + "@backstage/core-components": "^0.4.0", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/catalog-model": "^0.9.0", - "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/catalog-model": "^0.9.1", + "@backstage/integration-react": "^0.1.8", + "@backstage/plugin-catalog-react": "^0.4.5", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.10", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 281fed0211..a9944238ce 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-react +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/core-components@0.4.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 3217881046..90acb9acd3 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.4.0", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.3", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -33,8 +33,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.10", + "@backstage/dev-utils": "^0.2.8", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index dfc9a5722a..1befa4f6cc 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/integration +## 0.6.3 + +### Patch Changes + +- 5dca42b17: Update to properly handle Azure DevOps Server download URL + ## 0.6.2 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index ffe4e5776f..5ae86ea163 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.6.2", + "version": "0.6.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.10", "@backstage/config-loader": "^0.6.7", "@backstage/test-utils": "^0.1.17", "@types/jest": "^26.0.7", diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index 9937008b61..37fe50f3ea 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -97,5 +97,20 @@ describe('azure core', () => { ); expect(new URL(result).searchParams.get('scopePath')).toEqual('docs'); }); + + it.each([ + { + url: 'https://dev.azure.com/org-name/project-name/_git/repo-name', + result: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?recursionLevel=full&download=true&api-version=6.0', + }, + { + url: 'https://api.com/org-name/project-name/_git/repo-name', + result: + 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?recursionLevel=full&download=true&api-version=6.0', + }, + ])('should handle happy path %#', async ({ url, result }) => { + expect(getAzureDownloadUrl(url)).toBe(result); + }); }); }); diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 18de5aa1d2..e22764f643 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -100,7 +100,17 @@ export function getAzureDownloadUrl(url: string): string { ? `&scopePath=${encodeURIComponent(filepath)}` : ''; - return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; + if (resource === 'dev.azure.com') { + return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; + } + + // For Azure DevOps Server `parseGitUrl` returns the same values + // for `organization` and `project` like this: `organization/project/_git` + // so we drop `project` and then strip `/_git` from `organization` + return `${protocol}://${resource}/${organization.replace( + '/_git', + '', + )}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; } /** diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index f7d12eb006..34f956586e 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/search-common +## 0.2.0 + +### Minor Changes + +- a13f21cdc: Implement optional `pageCursor` based paging in search. + + To use paging in your app, add a `` to your + `SearchPage.tsx`. + ## 0.1.3 ### Patch Changes diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 055f2d152f..f00beb6830 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -53,7 +53,7 @@ export interface SearchQuery { // (undocumented) filters?: JsonObject; // (undocumented) - pageCursor: string; + pageCursor?: string; // (undocumented) term: string; // (undocumented) @@ -74,6 +74,10 @@ export interface SearchResult { // // @public (undocumented) export interface SearchResultSet { + // (undocumented) + nextPageCursor?: string; + // (undocumented) + previousPageCursor?: string; // (undocumented) results: SearchResult[]; } diff --git a/packages/search-common/package.json b/packages/search-common/package.json index ede7baeae2..be1aad16b0 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,7 +39,7 @@ "@backstage/config": "^0.1.6" }, "devDependencies": { - "@backstage/cli": "^0.7.7" + "@backstage/cli": "^0.7.10" }, "jest": { "roots": [ diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 2a4447efac..063e0cb96c 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -19,7 +19,7 @@ export interface SearchQuery { term: string; filters?: JsonObject; types?: string[]; - pageCursor: string; + pageCursor?: string; } export interface SearchResult { @@ -29,6 +29,8 @@ export interface SearchResult { export interface SearchResultSet { results: SearchResult[]; + nextPageCursor?: string; + previousPageCursor?: string; } /** diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 342bb76c21..360bebded0 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/techdocs-common +## 0.10.0 + +### Minor Changes + +- 8b0f6f860: Set the correct `edit_uri` or `repo_url` for documentation pages that are hosted on GitHub and GitLab. + + The constructor of the `TechDocsGenerator` changed. + Prefer the use of `TechdocsGenerator.fromConfig(…)` instead: + + ```diff + - const techdocsGenerator = new TechdocsGenerator({ + + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { + logger, + containerRunner, + - config, + }); + ``` + +### Patch Changes + +- 30ed662a3: Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. + + Adding extra information to indexed tech docs documents for search. + +- 3624616e7: "Local" (out-of-the-box) publisher explicitly follows lower-case entity triplet + logic. +- 67ba7e088: Only write the updated `mkdocs.yml` file if the content was updated. + + This keeps local files unchanged if the `dir` annotation is used in combination with the `file` location. + +- 8eab6be6a: Force using `posix` path for cloud storage +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/search-common@0.2.0 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + ## 0.9.0 ### Minor Changes diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 22613f71a2..edf4bac6db 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -249,10 +249,12 @@ export class TechdocsGenerator implements GeneratorBase { logger, containerRunner, config, + scmIntegrations, }: { logger: Logger_2; containerRunner: ContainerRunner; config: Config; + scmIntegrations: ScmIntegrationRegistry; }); // (undocumented) static fromConfig( @@ -264,7 +266,7 @@ export class TechdocsGenerator implements GeneratorBase { containerRunner: ContainerRunner; logger: Logger_2; }, - ): Promise; + ): TechdocsGenerator; // (undocumented) run({ inputDir, diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 02a0622244..a914830fd5 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.9.0", + "version": "0.10.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,18 +38,19 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-model": "^0.9.0", + "@backstage/backend-common": "^0.9.1", + "@backstage/catalog-model": "^0.9.1", "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/search-common": "^0.1.3", - "@backstage/integration": "^0.6.2", + "@backstage/search-common": "^0.2.0", + "@backstage/integration": "^0.6.3", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.4", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", "express": "^4.17.1", "fs-extra": "9.1.0", + "git-url-parse": "~11.4.4", "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", @@ -59,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.10", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml new file mode 100644 index 0000000000..3924224560 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +# This is a comment that is removed after editing diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml new file mode 100644 index 0000000000..c7f402ec12 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml @@ -0,0 +1,4 @@ +site_name: Test site name +site_description: Test site description + +edit_uri: https://github.com/backstage/backstage/edit/main/docs diff --git a/packages/techdocs-common/src/stages/generate/generators.test.ts b/packages/techdocs-common/src/stages/generate/generators.test.ts index a2ace2628c..56343763e9 100644 --- a/packages/techdocs-common/src/stages/generate/generators.test.ts +++ b/packages/techdocs-common/src/stages/generate/generators.test.ts @@ -44,10 +44,9 @@ describe('generators', () => { it('should return correct registered generator', async () => { const generators = new Generators(); - const techdocs = new TechdocsGenerator({ + const techdocs = TechdocsGenerator.fromConfig(new ConfigReader({}), { logger, containerRunner, - config: new ConfigReader({}), }); generators.register('techdocs', techdocs); diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index af7958dcfb..3d1ad2b73b 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -38,10 +38,9 @@ export class Generators implements GeneratorBuilder { ): Promise { const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator({ + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { logger, containerRunner, - config, }); generators.register('techdocs', techdocsGenerator); diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 4039d340f2..fcf506098e 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import os from 'os'; import path, { resolve as resolvePath } from 'path'; import { ParsedLocationAnnotation } from '../../helpers'; -import { RemoteProtocol } from '../prepare/types'; import { addBuildTimestampMetadata, getGeneratorKey, getMkdocsYml, getRepoUrlFromLocationAnnotation, - isValidRepoUrlForMkdocs, patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, @@ -48,6 +49,9 @@ const mkdocsYmlWithExtensions = fs.readFileSync( const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); +const mkdocsYmlWithEditUri = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_edit_uri.yml'), +); const mkdocsYmlWithValidDocDir = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_valid_doc_dir.yml'), ); @@ -57,9 +61,14 @@ const mkdocsYmlWithInvalidDocDir = fs.readFileSync( const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir2.yml'), ); +const mkdocsYmlWithComments = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), +); const mockLogger = getVoidLogger(); const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({})); + describe('helpers', () => { describe('getGeneratorKey', () => { it('should return techdocs as the only generator key', () => { @@ -68,120 +77,85 @@ describe('helpers', () => { }); }); - describe('isValidRepoUrlForMkdocs', () => { - it('should return true for valid repo_url values for mkdocs', () => { - const validRepoUrls = [ - 'https://github.com/org/repo', - 'https://github.com/backstage/backstage/', - 'https://github.com/org123/repo1-2-3/', - 'http://github.com/insecureOrg/insecureRepo', - 'https://gitlab.com/org/repo', - 'https://gitlab.com/backstage/backstage/', - 'https://gitlab.com/org123/repo1-2-3/', - 'http://gitlab.com/insecureOrg/insecureRepo', - ]; - - const validRemoteProtocols = ['github', 'gitlab']; - - validRepoUrls.forEach(url => { - validRemoteProtocols.forEach(targetType => { - expect( - isValidRepoUrlForMkdocs(url, targetType as RemoteProtocol), - ).toBe(true); - }); - }); - }); - - it('should return false for invalid repo_urls values for mkdocs', () => { - const invalidRepoUrls = [ - 'git@github.com:org/repo', - 'https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend', - ]; - - invalidRepoUrls.forEach(url => { - expect(isValidRepoUrlForMkdocs(url, 'github')).toBe(false); - }); - }); - - it('should return false for unsupported remote protocols', () => { - const validRepoUrl = 'https://github.com/backstage/backstage'; - - const unsupportedRemoteProtocols = ['dir', 'file', 'url']; - - unsupportedRemoteProtocols.forEach(targetType => { - expect( - isValidRepoUrlForMkdocs(validRepoUrl, targetType as RemoteProtocol), - ).toBe(false); - }); - }); - }); - describe('getRepoUrlFromLocationAnnotation', () => { - it('should return undefined for unsupported location type', () => { - const parsedLocationAnnotation1: ParsedLocationAnnotation = { + it.each` + url | repo_url | edit_uri + ${'https://github.com/backstage/backstage'} | ${'https://github.com/backstage/backstage'} | ${undefined} + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${undefined} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} + ${'https://github.com/backstage/backstage/tree/main/'} | ${undefined} | ${'https://github.com/backstage/backstage/edit/main/docs'} + ${'https://gitlab.com/backstage/backstage'} | ${'https://gitlab.com/backstage/backstage'} | ${undefined} + ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${undefined} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/docs'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/'} | ${undefined} | ${'https://gitlab.com/backstage/backstage/-/edit/main/docs'} + `('should convert $url', ({ url: target, repo_url, edit_uri }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({ repo_url, edit_uri }); + }); + + it.each` + url | edit_uri + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/custom/folder'} + ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/edit/main/custom/folder'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/custom/folder'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/custom/folder'} + `( + 'should convert $url with custom docsFolder', + ({ url: target, edit_uri }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + './custom/folder', + ), + ).toEqual({ edit_uri }); + }, + ); + + it.each` + url + ${'https://bitbucket.org/backstage/backstage/src/master/examples/techdocs/'} + ${'https://bitbucket.org/backstage/backstage/src/master/'} + ${'https://dev.azure.com/organization/project/_git/repository?path=%2Fexamples%2Ftechdocs'} + ${'https://dev.azure.com/organization/project/_git/repository?path=%2F'} + `('should ignore $url', ({ url: target }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({}); + }); + + it('should ignore unsupported location type', () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { type: 'dir', target: '/home/user/workspace/docs-repository', }; - const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'file' as RemoteProtocol, - target: '/home/user/workspace/docs-repository/catalog-info.yaml', - }; - - const parsedLocationAnnotation3: ParsedLocationAnnotation = { - type: 'url', - target: 'https://my-website.com/storage/this/docs/repository', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe( - undefined, - ); - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe( - undefined, - ); - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe( - undefined, - ); - }); - - it('should return correct target url for supported hosts', () => { - const parsedLocationAnnotation1: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: 'https://github.com/backstage/backstage.git', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe( - 'https://github.com/backstage/backstage', - ); - - const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: 'https://github.com/org/repo', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe( - 'https://github.com/org/repo', - ); - - const parsedLocationAnnotation3: ParsedLocationAnnotation = { - type: 'gitlab' as RemoteProtocol, - target: 'https://gitlab.com/org/repo', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe( - 'https://gitlab.com/org/repo', - ); - - const parsedLocationAnnotation4: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: - 'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation4)).toBe( - 'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', - ); + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({}); }); }); @@ -190,7 +164,9 @@ describe('helpers', () => { mockFs({ '/mkdocs.yml': mkdocsYml, '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, + '/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, + '/mkdocs_with_comments.yml': mkdocsYmlWithComments, }); }); @@ -198,9 +174,9 @@ describe('helpers', () => { mockFs.restore(); }); - it('should add repo_url to mkdocs.yml', async () => { + it('should add edit_uri to mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/backstage/backstage', }; @@ -208,6 +184,7 @@ describe('helpers', () => { '/mkdocs.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs.yml'); @@ -219,7 +196,7 @@ describe('helpers', () => { it('should add repo_url to mkdocs.yml that contains custom yaml tags', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/backstage/backstage', }; @@ -227,6 +204,7 @@ describe('helpers', () => { '/mkdocs_with_extensions.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs_with_extensions.yml'); @@ -241,7 +219,7 @@ describe('helpers', () => { it('should not override existing repo_url in mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/neworg/newrepo', }; @@ -249,6 +227,7 @@ describe('helpers', () => { '/mkdocs_with_repo_url.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml'); @@ -260,6 +239,51 @@ describe('helpers', () => { 'repo_url: https://github.com/neworg/newrepo', ); }); + + it('should not override existing edit_uri in mkdocs.yml', async () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target: 'https://github.com/neworg/newrepo', + }; + + await patchMkdocsYmlPreBuild( + '/mkdocs_with_edit_uri.yml', + mockLogger, + parsedLocationAnnotation, + scmIntegrations, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_with_edit_uri.yml'); + + expect(updatedMkdocsYml.toString()).toContain( + 'edit_uri: https://github.com/backstage/backstage/edit/main/docs', + ); + expect(updatedMkdocsYml.toString()).not.toContain( + 'https://github.com/neworg/newrepo', + ); + }); + + it('should not update mkdocs.yml if nothing should be changed', async () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'dir', + target: '/unsupported/path', + }; + + await patchMkdocsYmlPreBuild( + '/mkdocs_with_comments.yml', + mockLogger, + parsedLocationAnnotation, + scmIntegrations, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_with_comments.yml'); + + expect(updatedMkdocsYml.toString()).toContain( + '# This is a comment that is removed after editing', + ); + expect(updatedMkdocsYml.toString()).not.toContain('edit_uri'); + expect(updatedMkdocsYml.toString()).not.toContain('repo_url'); + }); }); describe('addBuildTimestampMetadata', () => { diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 7c60c7667f..0ac05e7dd2 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { isChildPath } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ScmIntegrationRegistry } from '@backstage/integration'; import { spawn } from 'child_process'; import fs from 'fs-extra'; +import gitUrlParse from 'git-url-parse'; import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml'; import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; @@ -80,63 +82,42 @@ export const runCommand = async ({ }; /** - * Return true if mkdocs can compile docs with provided repo_url - * - * Valid repo_url examples in mkdocs.yml - * - https://github.com/backstage/backstage - * - https://gitlab.com/org/repo/ - * - http://github.com/backstage/backstage - * - A http(s) protocol URL to the root of the repository - * - * Invalid repo_url examples in mkdocs.yml - * - https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component - * - (anything that is not valid as described above) - * - * @param {string} repoUrl URL supposed to be used as repo_url in mkdocs.yml - * @param {string} locationType Type of source code host - github, gitlab, dir, url, etc. - * @returns {boolean} - */ -export const isValidRepoUrlForMkdocs = ( - repoUrl: string, - locationType: string, -): boolean => { - // Trim trailing slash - const cleanRepoUrl = repoUrl.replace(/\/$/, ''); - - if (locationType === 'github' || locationType === 'gitlab') { - // A valid repoUrl to the root of the repository will be split into 5 strings if split using the / delimiter. - // We do not want URLs which have more than that number of forward slashes since they will signify a non-root location - // Note: This is not the best possible implementation but will work most of the times.. Feel free to improve or - // highlight edge cases. - return cleanRepoUrl.split('/').length === 5; - } - - return false; -}; - -/** - * Return a valid URL of the repository used in backstage.io/techdocs-ref annotation. - * Return undefined if the `target` is not valid in context of repo_url in mkdocs.yml - * Alter URL so that it is a valid repo_url config in mkdocs.yml + * Return the source url for MkDocs based on the backstage.io/techdocs-ref annotation. + * Depending on the type of target, it can either return a repo_url, an edit_uri, both, or none. * * @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type - * @returns {string | undefined} + * @param {ScmIntegrationRegistry} scmIntegrations the scmIntegration to do url transformations + * @param {string} docsFolder the configured docs folder in the mkdocs.yml (defaults to 'docs') + * @returns the settings for the mkdocs.yml */ export const getRepoUrlFromLocationAnnotation = ( parsedLocationAnnotation: ParsedLocationAnnotation, -): string | undefined => { + scmIntegrations: ScmIntegrationRegistry, + docsFolder: string = 'docs', +): { repo_url?: string; edit_uri?: string } => { const { type: locationType, target } = parsedLocationAnnotation; - // Add more options from the RemoteProtocol type of parsedLocationAnnotation.type here - // when TechDocs supports more hosts and if mkdocs can generated an Edit URL for them. - const supportedHosts = ['github', 'gitlab']; + if (locationType === 'url') { + const integration = scmIntegrations.byUrl(target); - if (supportedHosts.includes(locationType)) { - // Trim .git or .git/ from the end of repository url - return target.replace(/.git\/*$/, ''); + // We only support it for github and gitlab for now as the edit_uri + // is not properly supported for others yet. + if (integration && ['github', 'gitlab'].includes(integration.type)) { + // handle the case where a user manually writes url:https://github.com/backstage/backstage i.e. without /blob/... + const { filepathtype } = gitUrlParse(target); + if (filepathtype === '') { + return { repo_url: target }; + } + + const sourceFolder = integration.resolveUrl({ + url: `./${docsFolder}`, + base: target, + }); + return { edit_uri: integration.resolveEditUrl(sourceFolder) }; + } } - return undefined; + return {}; }; class UnknownTag { @@ -217,7 +198,7 @@ export const validateMkdocsYaml = async ( * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. * * List of tasks: - * - Add repo_url if it does not exists + * - Add repo_url or edit_uri if it does not exists * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get * the repository URL. @@ -228,12 +209,18 @@ export const validateMkdocsYaml = async ( * @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site * @param {Logger} logger * @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type + * @param {ScmIntegrationRegistry} scmIntegrations the scmIntegration to do url transformations */ export const patchMkdocsYmlPreBuild = async ( mkdocsYmlPath: string, logger: Logger, parsedLocationAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, ) => { + // We only want to override the mkdocs.yml if it has actually changed. This is relevant if + // used with a 'dir' location on the file system as this would permanently update the file. + let didEdit = false; + let mkdocsYmlFileString; try { mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); @@ -260,25 +247,37 @@ export const patchMkdocsYmlPreBuild = async ( return; } - // Add repo_url to mkdocs.yml if it is missing. This will enable the Page edit button generated by MkDocs. - if (!('repo_url' in mkdocsYml)) { - const repoUrl = getRepoUrlFromLocationAnnotation(parsedLocationAnnotation); - if (repoUrl !== undefined) { - // mkdocs.yml will not build with invalid repo_url. So, make sure it is valid. - // TODO: this is no longer working/meaningful because annotation type is - // now only ever "url" or "dir." Should be re-implemented! - if (isValidRepoUrlForMkdocs(repoUrl, parsedLocationAnnotation.type)) { - mkdocsYml.repo_url = repoUrl; - } + // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. + // This will enable the Page edit button generated by MkDocs. + // If the either has been set, keep the original value + if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { + const result = getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + mkdocsYml.docs_dir, + ); + + if (result.repo_url || result.edit_uri) { + mkdocsYml.repo_url = result.repo_url; + mkdocsYml.edit_uri = result.edit_uri; + didEdit = true; + + logger.info( + `Set ${JSON.stringify( + result, + )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, + ); } } try { - await fs.writeFile( - mkdocsYmlPath, - yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), - 'utf8', - ); + if (didEdit) { + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), + 'utf8', + ); + } } catch (error) { logger.warn( `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 1a6577cad9..c3d48507ce 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -18,6 +18,10 @@ import { ContainerRunner } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import path from 'path'; import { Logger } from 'winston'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { addBuildTimestampMetadata, getMkdocsYml, @@ -39,29 +43,39 @@ export class TechdocsGenerator implements GeneratorBase { private readonly logger: Logger; private readonly containerRunner: ContainerRunner; private readonly options: GeneratorConfig; + private readonly scmIntegrations: ScmIntegrationRegistry; - static async fromConfig( + static fromConfig( config: Config, { containerRunner, logger, }: { containerRunner: ContainerRunner; logger: Logger }, ) { - return new TechdocsGenerator({ logger, containerRunner, config }); + const scmIntegrations = ScmIntegrations.fromConfig(config); + return new TechdocsGenerator({ + logger, + containerRunner, + config, + scmIntegrations, + }); } constructor({ logger, containerRunner, config, + scmIntegrations, }: { logger: Logger; containerRunner: ContainerRunner; config: Config; + scmIntegrations: ScmIntegrationRegistry; }) { this.logger = logger; this.options = readGeneratorConfig(config, logger); this.containerRunner = containerRunner; + this.scmIntegrations = scmIntegrations; } public async run({ @@ -74,16 +88,19 @@ export class TechdocsGenerator implements GeneratorBase { }: GeneratorRunOptions): Promise { // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); + + // validate the docs_dir first + await validateMkdocsYaml(inputDir, content); + if (parsedLocationAnnotation) { await patchMkdocsYmlPreBuild( mkdocsYmlPath, childLogger, parsedLocationAnnotation, + this.scmIntegrations, ); } - await validateMkdocsYaml(inputDir, content); - // Directories to bind on container const mountDirs = { [inputDir]: '/input', diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index 0ced296a88..38ead236bd 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -25,10 +25,10 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import { LocalPublish } from './local'; -const createMockEntity = (annotations = {}) => { +const createMockEntity = (annotations = {}, lowerCase = false) => { return { apiVersion: 'version', - kind: 'TestKind', + kind: lowerCase ? 'testkind' : 'TestKind', metadata: { name: 'test-component-name', annotations: { @@ -65,11 +65,42 @@ describe('local publisher', () => { const publisher = new LocalPublish(mockConfig, logger, testDiscovery); const mockEntity = createMockEntity(); + const lowerMockEntity = createMockEntity(undefined, true); await publisher.publish({ entity: mockEntity, directory: tmpDir }); expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); + // Lower/upper should be treated the same. + expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(true); + + mockFs.restore(); + }); + + it('should respect legacy casing', async () => { + mockFs({ + [tmpDir]: { + 'index.html': '', + }, + }); + + const mockConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + + const publisher = new LocalPublish(mockConfig, logger, testDiscovery); + const mockEntity = createMockEntity(); + const lowerMockEntity = createMockEntity(undefined, true); + + await publisher.publish({ entity: mockEntity, directory: tmpDir }); + + expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); + + // Lower/upper should be treated differently. + expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(false); + mockFs.restore(); }); @@ -86,6 +117,13 @@ describe('local publisher', () => { [resolvedDir]: { 'unsafe.html': '', 'unsafe.svg': '', + default: { + testkind: { + testname: { + 'index.html': 'found it', + }, + }, + }, }, }); }); @@ -107,5 +145,38 @@ describe('local publisher', () => { 'content-type': 'text/plain; charset=utf-8', }); }); + + it('should redirect case-sensitive triplet path to lower-case', async () => { + const response = await request(app) + .get('/default/TestKind/TestName/index.html') + .expect('Location', '/default/testkind/testname/index.html'); + expect(response.status).toBe(301); + }); + + it('should resolve lower-case triplet path content eventually', async () => { + const response = await request(app) + .get('/default/TestKind/TestName/index.html') + .redirects(1); + expect(response.text).toEqual('found it'); + }); + + it('should not redirect when legacy case setting is used', async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + const legacyPublisher = new LocalPublish( + legacyConfig, + logger, + testDiscovery, + ); + app = express().use(legacyPublisher.docsRouter()); + + const response = await request(app).get( + '/default/TestKind/TestName/index.html', + ); + expect(response.status).toBe(404); + }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index 24af34932f..c547b65406 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -58,6 +58,8 @@ try { * called "static" at the root of techdocs-backend plugin. */ export class LocalPublish implements PublisherBase { + private legacyPathCasing: boolean; + // TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers. // Move the logic of setting staticDocsDir based on config over to fromConfig, // and set the value as a class parameter. @@ -70,6 +72,10 @@ export class LocalPublish implements PublisherBase { this.config = config; this.logger = logger; this.discovery = discovery; + this.legacyPathCasing = + config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; } async getReadiness(): Promise { @@ -81,8 +87,7 @@ export class LocalPublish implements PublisherBase { publish({ entity, directory }: PublishRequest): Promise { const entityNamespace = entity.metadata.namespace ?? 'default'; - const publishDir = path.join( - staticDocsDir, + const publishDir = this.staticEntityPathJoin( entityNamespace, entity.kind, entity.metadata.name, @@ -119,8 +124,7 @@ export class LocalPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { - const metadataPath = path.join( - staticDocsDir, + const metadataPath = this.staticEntityPathJoin( entityName.namespace, entityName.kind, entityName.name, @@ -138,23 +142,61 @@ export class LocalPublish implements PublisherBase { } docsRouter(): express.Handler { - return express.static(staticDocsDir, { - // Handle content-type header the same as all other publishers. - setHeaders: (res, filePath) => { - const fileExtension = path.extname(filePath); - const headers = getHeadersForFileExtension(fileExtension); - for (const [header, value] of Object.entries(headers)) { - res.setHeader(header, value); - } - }, + const router = express.Router(); + + // Redirect middleware ensuring that requests to case-sensitive entity + // triplet paths are always sent to lower-case versions. + router.use((req, res, next) => { + // If legacy path casing is on, let the request immediately continue. + if (this.legacyPathCasing) { + return next(); + } + + // Generate a lower-case entity triplet path. + const [_, namespace, kind, name, ...rest] = req.path.split('/'); + + // Ignore non-triplet objects. + if (!namespace || !kind || !name) { + return next(); + } + + const newPath = [ + _, + namespace.toLowerCase(), + kind.toLowerCase(), + name.toLowerCase(), + ...rest, + ].join('/'); + + // If there was no change, then let express.static() handle the request. + if (newPath === req.path) { + return next(); + } + + // Otherwise, redirect to the new path. + return res.redirect(req.baseUrl + newPath, 301); }); + + router.use( + express.static(staticDocsDir, { + // Handle content-type header the same as all other publishers. + setHeaders: (res, filePath) => { + const fileExtension = path.extname(filePath); + const headers = getHeadersForFileExtension(fileExtension); + for (const [header, value] of Object.entries(headers)) { + res.setHeader(header, value); + } + }, + }), + ); + + return router; } async hasDocsBeenGenerated(entity: Entity): Promise { const namespace = entity.metadata.namespace ?? 'default'; - const indexHtmlPath = path.join( - staticDocsDir, + const indexHtmlPath = this.staticEntityPathJoin( namespace, entity.kind, entity.metadata.name, @@ -210,4 +252,22 @@ export class LocalPublish implements PublisherBase { ), ); } + + /** + * Utility wrapper around path.join(), used to control legacy case logic. + */ + protected staticEntityPathJoin(...allParts: string[]): string { + if (this.legacyPathCasing) { + const [namespace, kind, name, ...parts] = allParts; + return path.join(staticDocsDir, namespace, kind, name, ...parts); + } + const [namespace, kind, name, ...parts] = allParts; + return path.join( + staticDocsDir, + namespace.toLowerCase(), + kind.toLowerCase(), + name.toLowerCase(), + ...parts, + ); + } } diff --git a/plugins/allure/.eslintrc.js b/plugins/allure/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/allure/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md new file mode 100644 index 0000000000..d62b95ea66 --- /dev/null +++ b/plugins/allure/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-allure + +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/core-components@0.4.0 + - @backstage/catalog-model@0.9.1 diff --git a/plugins/allure/README.md b/plugins/allure/README.md new file mode 100644 index 0000000000..b9a8435a34 --- /dev/null +++ b/plugins/allure/README.md @@ -0,0 +1,47 @@ +# [Allure](https://docs.qameta.io/allure/) + +Welcome to the Backstage Allure plugin. This plugin add an entity service page to display Allure test reports related to the service. + +## Install + +Run the below command from the `app` package directory. + +```shell +yarn add @backstage/plugin-allure +``` + +alternatively, you can execute below command from the root directory of your Backstage app. + +```shell +yarn workspace app add @backstage/plugin-allure +``` + +## Configure + +### Configure Allure service + +Add below configuration in the `app-config.yaml`. + +```yaml +allure: + baseUrl: # Example: https://allure.my-company.net or when running allure locally, http://localhost:5050/allure-docker-service +``` + +### Setup entity service page + +Add `EntityAllureReportContent` in the `EntityPage.tsx` like below: + +```diff ++ import { EntityAllureReportContent } from '@backstage/plugin-allure'; + +... + +const serviceEntityPage = ( + + ... ++ ++ ++ + +); +``` diff --git a/plugins/allure/api-report.md b/plugins/allure/api-report.md new file mode 100644 index 0000000000..c39b092954 --- /dev/null +++ b/plugins/allure/api-report.md @@ -0,0 +1,27 @@ +## API Report File for "@backstage/plugin-allure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "allurePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const allurePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "EntityAllureReportContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityAllureReportContent: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/allure/dev/example-entity.yaml b/plugins/allure/dev/example-entity.yaml new file mode 100644 index 0000000000..58a9ffd110 --- /dev/null +++ b/plugins/allure/dev/example-entity.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: allure-example-service + description: Allure Example Service + annotations: + qameta.io/allure-project: sample +spec: + type: service + lifecycle: experimental + owner: team-a diff --git a/plugins/allure/dev/index.tsx b/plugins/allure/dev/index.tsx new file mode 100644 index 0000000000..d697febd56 --- /dev/null +++ b/plugins/allure/dev/index.tsx @@ -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. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { EntityAllureReportContent } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; + +import exampleEntity from './example-entity.yaml'; +import { allureApiRef } from '../src/api'; + +createDevApp() + .registerApi({ + api: allureApiRef, + deps: {}, + factory: () => + ({ + async getReportUrl(projectId: string) { + return Promise.resolve( + // Follow the instructions from https://github.com/fescobar/allure-docker-service-ui + // to setup Allure service locally + `http://localhost:5050/allure-docker-service/projects/${projectId}/reports/latest/index.html`, + ); + }, + } as unknown as typeof allureApiRef.T), + }) + .addPage({ + element: ( + +
+ + + + + + + ), + title: 'Allure Report', + path: '/allure', + }) + .render(); diff --git a/plugins/allure/package.json b/plugins/allure/package.json new file mode 100644 index 0000000000..bc56349a1c --- /dev/null +++ b/plugins/allure/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/plugin-allure", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.1", + "@backstage/core-components": "^0.4.0", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/theme": "^0.2.10", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.10", + "@backstage/core-app-api": "^0.1.11", + "@backstage/dev-utils": "^0.2.8", + "@backstage/test-utils": "^0.1.17", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ], + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@backstage/allure", + "type": "object", + "properties": { + "allure": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "visibility": "frontend" + } + } + } + } + } +} diff --git a/plugins/allure/src/api/AllureApi.ts b/plugins/allure/src/api/AllureApi.ts new file mode 100644 index 0000000000..a926da03d0 --- /dev/null +++ b/plugins/allure/src/api/AllureApi.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +import { createApiRef } from '@backstage/core-plugin-api'; + +export type AllureApi = { + getReportUrl(projectId: string): Promise; +}; + +export const allureApiRef = createApiRef({ + id: 'allure-api', +}); diff --git a/plugins/allure/src/api/AllureApiClient.ts b/plugins/allure/src/api/AllureApiClient.ts new file mode 100644 index 0000000000..698713c837 --- /dev/null +++ b/plugins/allure/src/api/AllureApiClient.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ +import { ConfigApi } from '@backstage/core-plugin-api'; +import { AllureApi } from './AllureApi'; + +export class AllureApiClient implements AllureApi { + readonly configApi: ConfigApi; + + constructor(options: { configApi: ConfigApi }) { + this.configApi = options.configApi; + } + + async getReportUrl(projectId: string): Promise { + const baseUrl = this.configApi.getString('allure.baseUrl'); + return `${baseUrl}/projects/${projectId}/reports/latest/index.html`; + } +} diff --git a/plugins/allure/src/api/index.ts b/plugins/allure/src/api/index.ts new file mode 100644 index 0000000000..1c1adb1db2 --- /dev/null +++ b/plugins/allure/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ +export { allureApiRef } from './AllureApi'; +export { AllureApiClient } from './AllureApiClient'; diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx new file mode 100644 index 0000000000..19c7796e55 --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -0,0 +1,44 @@ +/* + * 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. + */ +import React from 'react'; +import { AllureReportComponent } from './AllureReportComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); + }); +}); diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx new file mode 100644 index 0000000000..2ca3e87691 --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx @@ -0,0 +1,65 @@ +/* + * 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. + */ +import React from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { allureApiRef } from '../../api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + ALLURE_PROJECT_ID_ANNOTATION, + isAllureReportAvailable, + getAllureProjectId, +} from '../annotationHelpers'; +import { + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import { useAsync } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; + +const AllureReport = (props: { entity: Entity }) => { + const allureApi = useApi(allureApiRef); + + const allureProjectId = getAllureProjectId(props.entity); + const { value, loading } = useAsync(async () => { + const url = await allureApi.getReportUrl(allureProjectId); + return url; + }); + + if (loading) { + return ; + } + return ( +