document built-in template filters

Signed-off-by: Matt Benson <gudnabrsam@gmail.com>
This commit is contained in:
Matt Benson
2025-02-07 14:16:51 -06:00
parent 7f56c442aa
commit 497d47aafa
15 changed files with 360 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
document built-in template filters
@@ -1,40 +0,0 @@
/*
* Copyright 2023 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 { parseEntityRef } from '@backstage/catalog-model';
import { ScmIntegrations } from '@backstage/integration';
import type { JsonObject, JsonValue } from '@backstage/types';
import {
parseRepoUrl,
TemplateFilter,
} from '@backstage/plugin-scaffolder-node';
import get from 'lodash/get';
export default ({
integrations,
}: {
integrations: ScmIntegrations;
}): Record<string, TemplateFilter> => {
return {
parseRepoUrl: url => parseRepoUrl(url as string, integrations),
parseEntityRef: (ref: JsonValue, context?: JsonValue) =>
parseEntityRef(ref as string, context as JsonObject),
pick: (obj: JsonValue, key: JsonValue) => get(obj, key as string),
projectSlug: repoUrl => {
const { owner, repo } = parseRepoUrl(repoUrl as string, integrations);
return `${owner}/${repo}`;
},
};
};
@@ -0,0 +1,29 @@
/*
* Copyright 2025 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 { ScmIntegrations } from '@backstage/integration';
import { createParseRepoUrl } from './parseRepoUrl';
import { parseEntityRef } from './parseEntityRef';
import { pick } from './pick';
import { createProjectSlug } from './projectSlug';
export const createDefaultFilters = (options: {
integrations: ScmIntegrations;
}) => [
createParseRepoUrl(options.integrations),
parseEntityRef,
pick,
createProjectSlug(options.integrations),
];
@@ -0,0 +1,48 @@
import { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
/*
* Copyright 2025 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 const examples: TemplateFilterExample[] = [
{
description: 'Without context',
example: `\
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef }}
`,
notes: `\
- **Input**: \`group:techdocs\`
- **Output**: \`{"kind": "group", "namespace": "default", "name": "techdocs"}\`
`,
},
{
description: 'With context',
example: `\
- id: log
name: Parse Entity Reference
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef({ defaultKind:"group", defaultNamespace:"another-namespace" }) }}
`,
notes: `\
- **Input**: \`techdocs\`
- **Arguments:**: \`[{ "defaultKind": "group", "defaultNamespace": "another-namespace" }]\`
- **Output**: \`{"kind": "group", "namespace": "another-namespace", "name": "techdocs"}\`
`,
},
];
@@ -0,0 +1,61 @@
/*
* Copyright 2025 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 { parseEntityRef as filter } from '@backstage/catalog-model';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const parseEntityRef = createTemplateFilter({
id: 'parseEntityRef',
description:
'Extracts the parts of an entity reference, such as the kind, namespace, and name.',
schema: z =>
z.function(
z.tuple([
z.union([
z.string().describe('compact entity reference'),
z
.object({
kind: z.string().optional(),
namespace: z.string().optional(),
name: z.string(),
})
.describe('`CompoundEntityRef`'),
]),
z
.object({
defaultKind: z
.string()
.describe('The default kind, if none is given in the reference'),
defaultNamespace: z
.string()
.describe(
'The default namespace, if none is given in the reference',
),
})
.partial()
.optional(),
]),
z
.object({
kind: z.string(),
namespace: z.string(),
name: z.string(),
})
.describe('`CompoundEntityRef`'),
),
examples,
filter,
});
@@ -0,0 +1,31 @@
/*
* Copyright 2025 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 { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
export const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Parse Repo URL
action: debug:log
input:
message: \${{ parameters.repoUrl | parseRepoUrl }}`,
notes: ` - **Input**: \`github.com?repo=backstage&owner=backstage\`
- **Output**: \`{"host":"github.com","owner":"backstage","repo":"backstage"}\`
`,
},
];
@@ -0,0 +1,50 @@
/*
* Copyright 2025 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 { ScmIntegrations } from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const createParseRepoUrl = (integrations: ScmIntegrations) =>
createTemplateFilter({
id: 'parseRepoUrl',
description:
'Parses a repository URL into its constituent parts: owner, repository name, etc.',
schema: z =>
z.function(
z.tuple([
z.string().describe('repo URL as collected from repository picker'),
]),
z
.object({
repo: z.string(),
host: z.string(),
})
.merge(
z
.object({
owner: z.string(),
organization: z.string(),
workspace: z.string(),
project: z.string(),
})
.partial(),
)
.describe('`RepoSpec`'),
),
examples,
filter: (url: string) => parseRepoUrl(url, integrations),
});
@@ -0,0 +1,31 @@
import { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
/*
* Copyright 2025 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 const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Pick
action: debug:log
input:
message: \${{ parameters.owner | parseEntityRef | pick('name') }}`,
notes: `\
- **Input**: \`{ kind: 'Group', namespace: 'default', name: 'techdocs'\` }
- **Output**: \`techdocs\`
`,
},
];
@@ -0,0 +1,31 @@
/*
* Copyright 2025 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 { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { get as filter } from 'lodash';
import { examples } from './examples';
export const pick = createTemplateFilter({
id: 'pick',
description:
'Selects a specific property (e.g. kind, namespace, name) from an object.',
schema: z =>
z.function(
z.tuple([z.any(), z.string().describe('Property')]),
z.any().describe('Selected property'),
),
examples,
filter,
});
@@ -0,0 +1,32 @@
import { TemplateFilterExample } from '@backstage/plugin-scaffolder-node/alpha';
/*
* Copyright 2025 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 const examples: TemplateFilterExample[] = [
{
example: `\
- id: log
name: Project Slug
action: debug:log
input:
message: \${{ parameters.repoUrl | projectSlug }}
`,
notes: `\
- **Input**: \`github.com?repo=backstage&owner=backstage\`
- **Output**: backstage/backstage
`,
},
];
@@ -0,0 +1,37 @@
/*
* Copyright 2025 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 { ScmIntegrations } from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import { createTemplateFilter } from '@backstage/plugin-scaffolder-node/alpha';
import { examples } from './examples';
export const createProjectSlug = (integrations: ScmIntegrations) =>
createTemplateFilter({
id: 'projectSlug',
description: 'Generates a project slug from a repository URL.',
schema: z =>
z.function(
z.tuple([
z.string().describe('repo URL as collected from repository picker'),
]),
z.string(),
),
examples,
filter: (repoUrl: string) => {
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
return `${owner}/${repo}`;
},
});
@@ -29,7 +29,7 @@ import globby from 'globby';
import fs from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
import { SecureTemplater } from '../../../../lib/templating/SecureTemplater';
import createDefaultFilters from '../../../../lib/templating/filters';
import { createDefaultFilters } from '../../../../lib/templating/filters';
import { examples } from './template.examples';
import { convertFiltersToRecord } from '../../../../util/templating';
@@ -25,7 +25,7 @@ import {
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import { SecureTemplater } from '../../../../lib/templating/SecureTemplater';
import createDefaultFilters from '../../../../lib/templating/filters';
import { createDefaultFilters } from '../../../../lib/templating/filters';
import path from 'path';
import fs from 'fs-extra';
import { convertFiltersToRecord } from '../../../../util/templating';
@@ -55,7 +55,7 @@ import {
TemplateFilter,
TemplateGlobal,
} from '@backstage/plugin-scaffolder-node';
import createDefaultFilters from '../../lib/templating/filters';
import { createDefaultFilters } from '../../lib/templating/filters';
import { scaffolderActionRules } from '../../service/rules';
import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
import { BackstageLoggerTransport, WinstonLogger } from './logger';
@@ -28,14 +28,14 @@ import {
extractGlobalValueMetadata,
} from './templating';
import { JsonValue } from '@backstage/types';
import builtInFilters from '../lib/templating/filters';
import { createDefaultFilters } from '../lib/templating/filters';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
describe('templating utilities', () => {
describe('built-in filters', () => {
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
const filters = builtInFilters({ integrations });
const filters = createDefaultFilters({ integrations });
it('generates equivalent filter metadata', () => {
const metadata = extractFilterMetadata(filters);
expect(metadata).toMatchObject(extractFilterMetadata(filters));