diff --git a/.changeset/nine-bags-dream.md b/.changeset/nine-bags-dream.md
new file mode 100644
index 0000000000..f0967775db
--- /dev/null
+++ b/.changeset/nine-bags-dream.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-adr': patch
+---
+
+Render table of front matter metadata when displaying MADR v3 formatted ADR
diff --git a/.changeset/real-donuts-brush.md b/.changeset/real-donuts-brush.md
new file mode 100644
index 0000000000..b6a875bd64
--- /dev/null
+++ b/.changeset/real-donuts-brush.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-adr-backend': patch
+---
+
+Use front matter parser for MADR v3 formatted ADRs when indexing status/date
diff --git a/.changeset/strange-geese-argue.md b/.changeset/strange-geese-argue.md
new file mode 100644
index 0000000000..7be0886abb
--- /dev/null
+++ b/.changeset/strange-geese-argue.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-adr-common': patch
+---
+
+Add utility function for parsing MADR front matter
diff --git a/app-config.yaml b/app-config.yaml
index e2b03786c4..95995025bc 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -274,6 +274,9 @@ catalog:
# Example component for TechDocs
- type: file
target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
+ # Example component for ADRs
+ - type: file
+ target: ../../plugins/adr/examples/component/catalog-info.yaml
# Backstage example templates
- type: file
target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
diff --git a/packages/app/package.json b/packages/app/package.json
index 010b2dd648..3b7726fe4a 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -15,6 +15,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
+ "@backstage/plugin-adr": "workspace:^",
"@backstage/plugin-airbrake": "workspace:^",
"@backstage/plugin-apache-airflow": "workspace:^",
"@backstage/plugin-api-docs": "workspace:^",
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 026544ab26..ca6d09e318 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -26,6 +26,7 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { EmptyState, InfoCard } from '@backstage/core-components';
+import { EntityAdrContent, isAdrAvailable } from '@backstage/plugin-adr';
import {
EntityApiDefinitionCard,
EntityConsumedApisCard,
@@ -484,6 +485,10 @@ const serviceEntityPage = (
{techdocsContent}
+
+
+
+
{
name: 'Documentation',
icon: ,
},
+ {
+ value: 'adr',
+ name: 'Architecture Decision Records',
+ icon: ,
+ },
]}
/>
@@ -131,6 +137,7 @@ const SearchPage = () => {
} />
} />
} />
+ } />
diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts
index 0e6982545f..4290fcd68a 100644
--- a/packages/backend/src/plugins/search.ts
+++ b/packages/backend/src/plugins/search.ts
@@ -15,6 +15,7 @@
*/
import { useHotCleanup } from '@backstage/backend-common';
+import { DefaultAdrCollatorFactory } from '@backstage/plugin-adr-backend';
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend';
import { createRouter } from '@backstage/plugin-search-backend';
@@ -68,6 +69,18 @@ export default async function createPlugin(
// Collators are responsible for gathering documents known to plugins. This
// particular collator gathers entities from the software catalog.
+ indexBuilder.addCollator({
+ schedule,
+ factory: DefaultAdrCollatorFactory.fromConfig({
+ cache: env.cache,
+ config: env.config,
+ discovery: env.discovery,
+ logger: env.logger,
+ reader: env.reader,
+ tokenManager: env.tokenManager,
+ }),
+ });
+
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
diff --git a/plugins/adr-backend/src/search/madrParser.ts b/plugins/adr-backend/src/search/madrParser.ts
index c992ef125e..0dd1437538 100644
--- a/plugins/adr-backend/src/search/madrParser.ts
+++ b/plugins/adr-backend/src/search/madrParser.ts
@@ -16,7 +16,10 @@
import { DateTime } from 'luxon';
import { marked } from 'marked';
-import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common';
+import {
+ MADR_DATE_FORMAT,
+ parseMadrWithFrontmatter,
+} from '@backstage/plugin-adr-common';
const getTitle = (tokens: marked.TokensList): string | undefined => {
return (
@@ -26,22 +29,6 @@ const getTitle = (tokens: marked.TokensList): string | undefined => {
)?.text;
};
-const getStatusForV3Format = (tokens: marked.TokensList): string | undefined =>
- (
- tokens.find(
- t =>
- t.type === 'heading' &&
- t.depth === 2 &&
- t.text.toLowerCase().includes('status:'),
- ) as marked.Tokens.Text
- )?.text
- .toLowerCase()
- .split('\n')
- .find(l => /^status:/.test(l))
- ?.replace(/^status:/i, '')
- .trim()
- .toLocaleLowerCase('en-US');
-
const getStatusForV2Format = (tokens: marked.TokensList): string | undefined =>
(tokens.find(t => t.type === 'list') as marked.Tokens.List)?.items
?.find(t => /^status:/i.test(t.text))
@@ -49,71 +36,45 @@ const getStatusForV2Format = (tokens: marked.TokensList): string | undefined =>
.trim()
.toLocaleLowerCase('en-US');
-const getDateForV3Format = (
- tokens: marked.TokensList,
- dateFormat: string,
-): string | undefined => {
- let adrDateTime: DateTime | undefined;
- const dateLine = (
- tokens.find(
- t =>
- t.type === 'heading' &&
- t.depth === 2 &&
- t.text.toLowerCase().includes('date:'),
- ) as marked.Tokens.Text
- )?.text
- .toLowerCase()
- .split('\n')
- .find(l => /^date:/.test(l));
-
- if (dateLine) {
- adrDateTime = DateTime.fromFormat(
- dateLine.replace(/^date:/i, '').trim(),
- dateFormat,
- );
- }
- return adrDateTime?.isValid
- ? adrDateTime.toFormat(MADR_DATE_FORMAT)
- : undefined;
-};
-
-const getDateForV2Format = (
- tokens: marked.TokensList,
- dateFormat: string,
-): string | undefined => {
+const getDateForV2Format = (tokens: marked.TokensList): string | undefined => {
const listTokens = (tokens.find(t => t.type === 'list') as marked.Tokens.List)
?.items;
- const adrDateTime = DateTime.fromFormat(
- listTokens
- ?.find(t => /^date:/i.test(t.text))
- ?.text.replace(/^date:/i, '')
- .trim() ?? '',
- dateFormat,
- );
- return adrDateTime?.isValid
- ? adrDateTime.toFormat(MADR_DATE_FORMAT)
- : undefined;
+ const adrDateTime = listTokens
+ ?.find(t => /^date:/i.test(t.text))
+ ?.text.replace(/^date:/i, '')
+ .trim();
+ return adrDateTime;
};
-const getStatus = (tokens: marked.TokensList): string | undefined => {
- return getStatusForV3Format(tokens) ?? getStatusForV2Format(tokens);
+const getStatus = (
+ tokens: marked.TokensList,
+ frontMatterStatus?: string,
+): string | undefined => {
+ return frontMatterStatus ?? getStatusForV2Format(tokens);
};
const getDate = (
tokens: marked.TokensList,
dateFormat: string,
-): string | undefined =>
- getDateForV3Format(tokens, dateFormat) ??
- getDateForV2Format(tokens, dateFormat);
+ frontMatterDate?: string,
+): string | undefined => {
+ const dateString = frontMatterDate ?? getDateForV2Format(tokens);
+ if (!dateString) {
+ return undefined;
+ }
+ const date = DateTime.fromFormat(dateString, dateFormat);
+ return date?.isValid ? date.toFormat(MADR_DATE_FORMAT) : undefined;
+};
export const madrParser = (content: string, dateFormat = MADR_DATE_FORMAT) => {
- const tokens = marked.lexer(content);
+ const preparsed = parseMadrWithFrontmatter(content);
+ const tokens = marked.lexer(preparsed.content);
if (!tokens.length) {
throw new Error('ADR has no content');
}
return {
title: getTitle(tokens),
- status: getStatus(tokens),
- date: getDate(tokens, dateFormat),
+ status: getStatus(tokens, preparsed.status),
+ date: getDate(tokens, dateFormat, preparsed.date),
};
};
diff --git a/plugins/adr-common/api-report.md b/plugins/adr-common/api-report.md
index 0236b355a5..a36854f0e7 100644
--- a/plugins/adr-common/api-report.md
+++ b/plugins/adr-common/api-report.md
@@ -35,4 +35,15 @@ export const MADR_DATE_FORMAT = 'yyyy-MM-dd';
// @public
export const madrFilePathFilter: AdrFilePathFilterFn;
+
+// @public
+export interface ParsedMadr {
+ attributes: Record;
+ content: string;
+ date?: string;
+ status?: string;
+}
+
+// @public
+export const parseMadrWithFrontmatter: (content: string) => ParsedMadr;
```
diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json
index adbb6bea58..63bd30cb97 100644
--- a/plugins/adr-common/package.json
+++ b/plugins/adr-common/package.json
@@ -31,7 +31,8 @@
"dependencies": {
"@backstage/catalog-model": "workspace:^",
"@backstage/integration": "workspace:^",
- "@backstage/plugin-search-common": "workspace:^"
+ "@backstage/plugin-search-common": "workspace:^",
+ "front-matter": "^4.0.2"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts
index cf71367cba..194f4899d1 100644
--- a/plugins/adr-common/src/index.ts
+++ b/plugins/adr-common/src/index.ts
@@ -20,6 +20,7 @@
import { Entity, getEntitySourceLocation } from '@backstage/catalog-model';
import { IndexableDocument } from '@backstage/plugin-search-common';
import { ScmIntegrationRegistry } from '@backstage/integration';
+import frontMatter from 'front-matter';
/**
* ADR plugin annotation.
@@ -101,3 +102,43 @@ export interface AdrDocument extends IndexableDocument {
*/
date?: string;
}
+
+/**
+ * Parsed MADR document with front matter (if present) parsed and extracted from the main markdown content.
+ * @public
+ */
+export interface ParsedMadr {
+ /**
+ * Main body of ADR content (with any front matter removed)
+ */
+ content: string;
+ /**
+ * ADR status
+ */
+ status?: string;
+ /**
+ * ADR date
+ */
+ date?: string;
+ /**
+ * All attributes parsed from front matter
+ */
+ attributes: Record;
+}
+
+/**
+ * Utility function to parse raw markdown content for an ADR and extract any metadata found as "front matter" at the top of the Markdown document.
+ * @param content - Raw markdown content which may (optionally) include front matter
+ * @public
+ */
+export const parseMadrWithFrontmatter = (content: string): ParsedMadr => {
+ const parsed = frontMatter>(content);
+ const status = parsed.attributes.status;
+ const date = parsed.attributes.date;
+ return {
+ content: parsed.body,
+ status: status ? String(status) : undefined,
+ date: date ? String(date) : undefined,
+ attributes: parsed.attributes,
+ };
+};
diff --git a/plugins/adr/README.md b/plugins/adr/README.md
index 7feb9b03b0..e6178f003b 100644
--- a/plugins/adr/README.md
+++ b/plugins/adr/README.md
@@ -113,7 +113,7 @@ import { AdrDocument } from '@backstage/plugin-adr-common';
## Custom ADR formats
-By default, this plugin will parse ADRs according to the format specified by the [Markdown Architecture Decision Record (MADR) v2.x template](https://github.com/adr/madr/tree/2.1.2). If your ADRs are written using a different format, you can apply the following customizations to correctly identify and parse your documents:
+By default, this plugin will parse ADRs according to the format specified by the [Markdown Architecture Decision Record (MADR) v2.x template](https://github.com/adr/madr/tree/2.1.2) or the [Markdown Any Decision Record (MADR) 3.x template](https://github.com/adr/madr/tree/3.0.0). If your ADRs are written using a different format, you can apply the following customizations to correctly identify and parse your documents:
### Custom Filename/Path Format
@@ -135,7 +135,7 @@ const myCustomFilterFn: AdrFilePathFilterFn = (path: string): boolean => {
### Custom Content Decorators
-Your ADR Markdown content will typically be rendered in the UI as is with the exception of relative links/embeds being rewritten as absolute URLs so they can be linked correctly (e.g. `./my-diagram.png` => `/my-diagram.png`). Depending on your ADR format, you may want to apply additional transformations to the content (e.g. parsing/ignoring front matter). You can do so by passing in a list of custom content decorators for the optional `contentDecorators` parameter. Note that passing in this parameter will override the default decorators. If you want to include the default ones, make sure to add them as well:
+Your ADR Markdown content will typically be rendered in the UI as is with the exception of relative links/embeds being rewritten as absolute URLs so they can be linked correctly (e.g. `./my-diagram.png` => `/my-diagram.png`). Depending on your ADR format, you may want to apply additional transformations to the content (e.g. hiding or formatting front matter in a different way). You can do so by passing in a list of custom content decorators for the optional `contentDecorators` parameter. Note that passing in this parameter will override the default decorators. If you want to include the default ones, make sure to add them as well:
```tsx
import {
@@ -154,6 +154,7 @@ const myCustomDecorator: AdrContentDecorator = ({ content }) => {
diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md
index dceca2411c..4cec5047fc 100644
--- a/plugins/adr/api-report.md
+++ b/plugins/adr/api-report.md
@@ -80,6 +80,7 @@ export const AdrReader: {
decorators: Readonly<{
createRewriteRelativeLinksDecorator(): AdrContentDecorator;
createRewriteRelativeEmbedsDecorator(): AdrContentDecorator;
+ createFrontMatterFormatterDecorator(): AdrContentDecorator;
}>;
};
diff --git a/plugins/adr/examples/component/catalog-info.yaml b/plugins/adr/examples/component/catalog-info.yaml
new file mode 100644
index 0000000000..580110243f
--- /dev/null
+++ b/plugins/adr/examples/component/catalog-info.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: component-with-adrs
+ title: Component With ADRs
+ description: A Service with ADRs registerd
+ annotations:
+ backstage.io/adr-location: https://github.com/adr/madr/tree/develop/docs/decisions
+spec:
+ type: service
+ lifecycle: experimental
+ owner: user:guest
diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx
index ad99ce3a5a..6b07eee4cd 100644
--- a/plugins/adr/src/components/AdrReader/AdrReader.tsx
+++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx
@@ -59,6 +59,7 @@ export const AdrReader = (props: {
const adrDecorators = decorators ?? [
adrDecoratorFactories.createRewriteRelativeLinksDecorator(),
adrDecoratorFactories.createRewriteRelativeEmbedsDecorator(),
+ adrDecoratorFactories.createFrontMatterFormatterDecorator(),
];
return adrDecorators.reduce(
diff --git a/plugins/adr/src/components/AdrReader/decorators.ts b/plugins/adr/src/components/AdrReader/decorators.ts
index f24bb48a00..980eab5179 100644
--- a/plugins/adr/src/components/AdrReader/decorators.ts
+++ b/plugins/adr/src/components/AdrReader/decorators.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { parseMadrWithFrontmatter } from '@backstage/plugin-adr-common';
import { AdrContentDecorator } from './types';
/**
@@ -44,4 +45,23 @@ export const adrDecoratorFactories = Object.freeze({
),
});
},
+ /**
+ * Formats YAML front-matter into a table format (if any exists in the markdown document)
+ */
+ createFrontMatterFormatterDecorator(): AdrContentDecorator {
+ return ({ content }) => {
+ const parsedFrontmatter = parseMadrWithFrontmatter(content);
+ let table = '';
+ const attrs = parsedFrontmatter.attributes;
+ if (Object.keys(attrs).length > 0) {
+ const stripNewLines = (val: unknown) =>
+ String(val).replaceAll('\n', '
');
+ const row = (vals: string[]) => `|${vals.join('|')}|\n`;
+ table = `${row(Object.keys(attrs))}`;
+ table += `${row(Object.keys(attrs).map(() => '---'))}`;
+ table += `${row(Object.values(attrs).map(stripNewLines))}\n\n`;
+ }
+ return { content: table + parsedFrontmatter.content };
+ };
+ },
});
diff --git a/yarn.lock b/yarn.lock
index 69699f7c10..473bf53ccb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4553,10 +4553,11 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-search-common": "workspace:^"
+ front-matter: ^4.0.2
languageName: unknown
linkType: soft
-"@backstage/plugin-adr@workspace:plugins/adr":
+"@backstage/plugin-adr@workspace:^, @backstage/plugin-adr@workspace:plugins/adr":
version: 0.0.0-use.local
resolution: "@backstage/plugin-adr@workspace:plugins/adr"
dependencies:
@@ -24345,6 +24346,7 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/integration-react": "workspace:^"
+ "@backstage/plugin-adr": "workspace:^"
"@backstage/plugin-airbrake": "workspace:^"
"@backstage/plugin-apache-airflow": "workspace:^"
"@backstage/plugin-api-docs": "workspace:^"
@@ -25482,6 +25484,15 @@ __metadata:
languageName: node
linkType: hard
+"front-matter@npm:^4.0.2":
+ version: 4.0.2
+ resolution: "front-matter@npm:4.0.2"
+ dependencies:
+ js-yaml: ^3.13.1
+ checksum: a5b4c36d75a820301ebf31db0f677332d189c4561903ab6853eaa0504b43634f98557dbf87752e09043dbd2c9dcc14b4bcf9151cb319c8ad7e26edb203c0cd23
+ languageName: node
+ linkType: hard
+
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"