diff --git a/.changeset/odd-bobcats-hang.md b/.changeset/odd-bobcats-hang.md new file mode 100644 index 0000000000..9b3aeccffc --- /dev/null +++ b/.changeset/odd-bobcats-hang.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Document the internal built-in filters, and ensure that the types are validated when using `createTemplateFilter` and `createTemplateGlobalFunction` from the `zod` schema. diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 37afe780a9..c3100d8259 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -529,7 +529,7 @@ export interface RouterOptions { // (undocumented) additionalTemplateFilters?: | Record - | CreatedTemplateFilter[]; + | CreatedTemplateFilter[]; // (undocumented) additionalTemplateGlobals?: | Record diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 4882b3cb2e..9fe78c7119 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -82,7 +82,7 @@ export const scaffolderPlugin = createBackendPlugin({ }, }); - const additionalTemplateFilters: CreatedTemplateFilter[] = []; + const additionalTemplateFilters: CreatedTemplateFilter[] = []; const additionalTemplateGlobals: CreatedTemplateGlobal[] = []; env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, { diff --git a/plugins/scaffolder-backend/src/lib/templating/filters.ts b/plugins/scaffolder-backend/src/lib/templating/filters.ts deleted file mode 100644 index bc3f95b18c..0000000000 --- a/plugins/scaffolder-backend/src/lib/templating/filters.ts +++ /dev/null @@ -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 => { - 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}`; - }, - }; -}; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/createDefaultFilters.ts b/plugins/scaffolder-backend/src/lib/templating/filters/createDefaultFilters.ts new file mode 100644 index 0000000000..5eb4de5ac3 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/createDefaultFilters.ts @@ -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), +]; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/examples.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/examples.ts new file mode 100644 index 0000000000..aef5b451bb --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/examples.ts @@ -0,0 +1,48 @@ +/* + * 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[] = [ + { + 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"}\` +`, + }, +]; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/filter.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/filter.ts new file mode 100644 index 0000000000..23ce340370 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/filter.ts @@ -0,0 +1,63 @@ +/* + * 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() + .args( + 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(), + ) + .returns( + z + .object({ + kind: z.string(), + namespace: z.string(), + name: z.string(), + }) + .describe('`CompoundEntityRef`'), + ), + examples, + filter, +}); diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/index.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/index.ts new file mode 100644 index 0000000000..b11d925161 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseEntityRef/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { parseEntityRef } from './filter'; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/examples.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/examples.ts new file mode 100644 index 0000000000..4348305737 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/examples.ts @@ -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"}\` +`, + }, +]; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/filter.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/filter.ts new file mode 100644 index 0000000000..8a7daea3bc --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/filter.ts @@ -0,0 +1,52 @@ +/* + * 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() + .args( + z.string().describe('repo URL as collected from repository picker'), + ) + .returns( + 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 => parseRepoUrl(url, integrations), + }); diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/index.ts b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/index.ts new file mode 100644 index 0000000000..ed505315b5 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/parseRepoUrl/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { createParseRepoUrl } from './filter'; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/pick/examples.ts b/plugins/scaffolder-backend/src/lib/templating/filters/pick/examples.ts new file mode 100644 index 0000000000..28593847ef --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/pick/examples.ts @@ -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\` +`, + }, +]; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/pick/filter.ts b/plugins/scaffolder-backend/src/lib/templating/filters/pick/filter.ts new file mode 100644 index 0000000000..a38855becc --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/pick/filter.ts @@ -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() + .args(z.any(), z.string().describe('Property')) + .returns(z.any().describe('Selected property')), + examples, + filter, +}); diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/pick/index.ts b/plugins/scaffolder-backend/src/lib/templating/filters/pick/index.ts new file mode 100644 index 0000000000..b3ca69bc38 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/pick/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { pick } from './filter'; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/examples.ts b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/examples.ts new file mode 100644 index 0000000000..2d668baee4 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/examples.ts @@ -0,0 +1,32 @@ +/* + * 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: Project Slug + action: debug:log + input: + message: \${{ parameters.repoUrl | projectSlug }} +`, + notes: `\ +- **Input**: \`github.com?repo=backstage&owner=backstage\` +- **Output**: backstage/backstage +`, + }, +]; diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/filter.ts b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/filter.ts new file mode 100644 index 0000000000..f6d0eaf681 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/filter.ts @@ -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() + .args( + z.string().describe('repo URL as collected from repository picker'), + ) + .returns(z.string()), + examples, + filter: repoUrl => { + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + return `${owner}/${repo}`; + }, + }); diff --git a/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/index.ts b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/index.ts new file mode 100644 index 0000000000..71a092232c --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/filters/projectSlug/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { createProjectSlug } from './filter'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 543c25ab3e..8dfa26057b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -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/createDefaultFilters'; import { examples } from './template.examples'; import { convertFiltersToRecord } from '../../../../util/templating'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts index f3b98d148a..2cd4ce883c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts @@ -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/createDefaultFilters'; import path from 'path'; import fs from 'fs-extra'; import { convertFiltersToRecord } from '../../../../util/templating'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index b771c5ded1..c352714d9d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -55,7 +55,7 @@ import { TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; -import createDefaultFilters from '../../lib/templating/filters'; +import { createDefaultFilters } from '../../lib/templating/filters/createDefaultFilters'; import { scaffolderActionRules } from '../../service/rules'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; import { BackstageLoggerTransport, WinstonLogger } from './logger'; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1186e7d4f6..8258d8c36f 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -179,7 +179,7 @@ export interface RouterOptions { taskBroker?: TaskBroker; additionalTemplateFilters?: | Record - | CreatedTemplateFilter[]; + | CreatedTemplateFilter[]; additionalTemplateGlobals?: | Record | CreatedTemplateGlobal[]; diff --git a/plugins/scaffolder-backend/src/util/templating.test.ts b/plugins/scaffolder-backend/src/util/templating.test.ts index 5ac6db601b..61d8637016 100644 --- a/plugins/scaffolder-backend/src/util/templating.test.ts +++ b/plugins/scaffolder-backend/src/util/templating.test.ts @@ -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/createDefaultFilters'; 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)); @@ -64,7 +64,7 @@ describe('templating utilities', () => { z.string().describe('separator').optional(), ) .returns(z.string()), - filter: (input: string, times: number, separator?: string) => + filter: (input, times, separator) => Array(times) .fill(input) .join(separator ?? ''), @@ -149,7 +149,7 @@ describe('templating utilities', () => { id: 'respond', schema: z => z.function().args(z.string().describe('prompt')).returns(z.string()), - fn: (prompt: string) => + fn: prompt => prompt === 'knock knock' ? "who's there?" : "nobody's home", }), ]; diff --git a/plugins/scaffolder-backend/src/util/templating.ts b/plugins/scaffolder-backend/src/util/templating.ts index 1f97e98b83..2607d5bef6 100644 --- a/plugins/scaffolder-backend/src/util/templating.ts +++ b/plugins/scaffolder-backend/src/util/templating.ts @@ -22,7 +22,7 @@ import { CreatedTemplateGlobal, CreatedTemplateGlobalFunction, CreatedTemplateGlobalValue, - TemplateGlobalFunctionSchema, + ZodFunctionSchema, } from '@backstage/plugin-scaffolder-node/alpha'; import { JsonValue } from '@backstage/types'; import { Schema } from 'jsonschema'; @@ -63,8 +63,12 @@ type ExportFilterSchema = { * Converts a Zod function schema to JSON schema */ function convertZodFunctionToJsonSchema( - t: ReturnType>, + t: ReturnType>, ): ExportFunctionSchema { + if (!('parameters' in t) || !('returnType' in t)) { + throw new Error('Invalid Zod function schema'); + } + const args = (t.parameters().items as ZodType[]).map( zt => zodToJsonSchema(zt) as Schema, ); @@ -142,7 +146,9 @@ export function extractFilterMetadata( if (filter.schema) { metadata.schema = convertToFilterSchema( - convertZodFunctionToJsonSchema(filter.schema(z)), + convertZodFunctionToJsonSchema( + filter.schema(z) as z.ZodFunction, + ), ); } @@ -167,10 +173,7 @@ export function extractFilterMetadata( */ function isGlobalFunction( global: CreatedTemplateGlobal, -): global is CreatedTemplateGlobalFunction< - TemplateGlobalFunctionSchema | undefined, - any -> { +): global is CreatedTemplateGlobalFunction { return 'fn' in global; } diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index fa9e022bb4..a96f312d2d 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -29,56 +29,31 @@ export type AutocompleteHandler = ({ // @alpha (undocumented) export type CreatedTemplateFilter< - TSchema extends - | TemplateFilterSchema - | undefined - | unknown = unknown, - TFilterSchema extends TSchema extends TemplateFilterSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : TemplateFilter = TSchema extends TemplateFilterSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : TemplateFilter, + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, > = { id: string; description?: string; examples?: TemplateFilterExample[]; - schema?: TSchema; - filter: TFilterSchema; + schema?: ZodFunctionSchema; + filter: (...args: z.infer>) => z.infer; }; // @alpha (undocumented) export type CreatedTemplateGlobal = | CreatedTemplateGlobalValue - | CreatedTemplateGlobalFunction; + | CreatedTemplateGlobalFunction; // @alpha (undocumented) export type CreatedTemplateGlobalFunction< - TSchema extends - | TemplateGlobalFunctionSchema - | undefined - | unknown = unknown, - TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : Exclude< - TemplateGlobal, - JsonValue - > = TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : Exclude, + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, > = { id: string; description?: string; examples?: TemplateGlobalFunctionExample[]; - schema?: TSchema; - fn: TFilterSchema; + schema?: ZodFunctionSchema; + fn: (...args: z.infer>) => z.infer; }; // @alpha (undocumented) @@ -90,23 +65,27 @@ export type CreatedTemplateGlobalValue = { // @alpha export const createTemplateFilter: < - TSchema extends TemplateFilterSchema | undefined, - TFunctionSchema extends TSchema extends TemplateFilterSchema - ? z.infer> - : (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined, ->( - filter: CreatedTemplateFilter, -) => CreatedTemplateFilter; + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +>(options: { + id: string; + description?: string; + examples?: TemplateFilterExample[]; + schema?: ZodFunctionSchema; + filter: (...args: z.infer>) => z.infer; +}) => CreatedTemplateFilter; // @alpha export const createTemplateGlobalFunction: < - TSchema extends TemplateGlobalFunctionSchema | undefined, - TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : (...args: JsonValue[]) => JsonValue | undefined, ->( - fn: CreatedTemplateGlobalFunction, -) => CreatedTemplateGlobalFunction; + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +>(options: { + id: string; + description?: string; + examples?: TemplateGlobalFunctionExample[]; + schema?: ZodFunctionSchema; + fn: (...args: z.infer>) => z.infer; +}) => CreatedTemplateGlobalFunction; // @alpha export const createTemplateGlobalValue: ( @@ -156,7 +135,9 @@ export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint | CreatedTemplateFilter[], + filters: + | Record + | CreatedTemplateFilter[], ): void; // (undocumented) addTemplateGlobals( @@ -194,16 +175,6 @@ export type TemplateFilterExample = { notes?: string; }; -// @alpha (undocumented) -export type TemplateFilterSchema< - Args extends z.ZodTuple< - | [z.ZodType] - | [z.ZodType, ...(z.ZodType | z.ZodUnknown)[]], - z.ZodType | z.ZodUnknown | null - >, - Result extends z.ZodType | z.ZodUndefined, -> = (zod: typeof z) => z.ZodFunction; - // @public (undocumented) export type TemplateGlobal = | ((...args: JsonValue[]) => JsonValue | undefined) @@ -216,15 +187,6 @@ export type TemplateGlobalFunctionExample = { notes?: string; }; -// @alpha (undocumented) -export type TemplateGlobalFunctionSchema< - Args extends z.ZodTuple< - [] | [z.ZodType, ...(z.ZodType | z.ZodUnknown)[]], - z.ZodType | z.ZodUnknown | null - >, - Result extends z.ZodType | z.ZodUndefined, -> = (zod: typeof z) => z.ZodFunction; - // @alpha export interface WorkspaceProvider { // (undocumented) @@ -244,5 +206,17 @@ export interface WorkspaceProvider { }): Promise; } +// @alpha (undocumented) +export type ZodFunctionSchema< + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +> = ( + zod: typeof z, +) => + | z.ZodFunction, TReturnType> + | z.ZodType< + (...args: z.infer>) => z.infer + >; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts index 1f4bac4ab7..f648658588 100644 --- a/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts +++ b/plugins/scaffolder-node/src/alpha/filters/createTemplateFilter.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { CreatedTemplateFilter, TemplateFilterSchema } from './types'; +import { ZodFunctionSchema } from '../types'; +import { CreatedTemplateFilter, TemplateFilterExample } from './types'; import { z } from 'zod'; /** @@ -23,10 +23,12 @@ import { z } from 'zod'; * @alpha */ export const createTemplateFilter = < - TSchema extends TemplateFilterSchema | undefined, - TFunctionSchema extends TSchema extends TemplateFilterSchema - ? z.infer> - : (arg: JsonValue, ...rest: JsonValue[]) => JsonValue | undefined, ->( - filter: CreatedTemplateFilter, -): CreatedTemplateFilter => filter; + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +>(options: { + id: string; + description?: string; + examples?: TemplateFilterExample[]; + schema?: ZodFunctionSchema; + filter: (...args: z.infer>) => z.infer; +}): CreatedTemplateFilter => options; diff --git a/plugins/scaffolder-node/src/alpha/filters/types.ts b/plugins/scaffolder-node/src/alpha/filters/types.ts index 2f65baaefc..1906ba1c9c 100644 --- a/plugins/scaffolder-node/src/alpha/filters/types.ts +++ b/plugins/scaffolder-node/src/alpha/filters/types.ts @@ -14,21 +14,10 @@ * limitations under the License. */ import { z } from 'zod'; -import { TemplateFilter } from '../../types'; -import { JsonValue } from '@backstage/types'; +import { ZodFunctionSchema } from '../types'; export type { TemplateFilter } from '../../types'; -/** @alpha */ -export type TemplateFilterSchema< - Args extends z.ZodTuple< - | [z.ZodType] - | [z.ZodType, ...(z.ZodType | z.ZodUnknown)[]], - z.ZodType | z.ZodUnknown | null - >, - Result extends z.ZodType | z.ZodUndefined, -> = (zod: typeof z) => z.ZodFunction; - /** @alpha */ export type TemplateFilterExample = { description?: string; @@ -38,23 +27,12 @@ export type TemplateFilterExample = { /** @alpha */ export type CreatedTemplateFilter< - TSchema extends - | TemplateFilterSchema - | undefined - | unknown = unknown, - TFilterSchema extends TSchema extends TemplateFilterSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : TemplateFilter = TSchema extends TemplateFilterSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : TemplateFilter, + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, > = { id: string; description?: string; examples?: TemplateFilterExample[]; - schema?: TSchema; - filter: TFilterSchema; + schema?: ZodFunctionSchema; + filter: (...args: z.infer>) => z.infer; }; diff --git a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts index 1f9e090d7f..2687b83bb4 100644 --- a/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts +++ b/plugins/scaffolder-node/src/alpha/globals/createTemplateGlobal.ts @@ -18,9 +18,9 @@ import { z } from 'zod'; import { CreatedTemplateGlobalFunction, CreatedTemplateGlobalValue, - TemplateGlobalFunctionSchema, + TemplateGlobalFunctionExample, } from './types'; -import { JsonValue } from '@backstage/types'; +import { ZodFunctionSchema } from '../types'; /** * This function is used to create new template global values in type-safe manner. @@ -39,10 +39,12 @@ export const createTemplateGlobalValue = ( * @alpha */ export const createTemplateGlobalFunction = < - TSchema extends TemplateGlobalFunctionSchema | undefined, - TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : (...args: JsonValue[]) => JsonValue | undefined, ->( - fn: CreatedTemplateGlobalFunction, -): CreatedTemplateGlobalFunction => fn; + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +>(options: { + id: string; + description?: string; + examples?: TemplateGlobalFunctionExample[]; + schema?: ZodFunctionSchema; + fn: (...args: z.infer>) => z.infer; +}): CreatedTemplateGlobalFunction => options; diff --git a/plugins/scaffolder-node/src/alpha/globals/types.ts b/plugins/scaffolder-node/src/alpha/globals/types.ts index cbe74f1610..27a3a700e5 100644 --- a/plugins/scaffolder-node/src/alpha/globals/types.ts +++ b/plugins/scaffolder-node/src/alpha/globals/types.ts @@ -15,7 +15,7 @@ */ import { JsonValue } from '@backstage/types'; import { z } from 'zod'; -import { TemplateGlobal } from '../../types'; +import { ZodFunctionSchema } from '../types'; export type { TemplateGlobal } from '../../types'; @@ -26,15 +26,6 @@ export type CreatedTemplateGlobalValue = { description?: string; }; -/** @alpha */ -export type TemplateGlobalFunctionSchema< - Args extends z.ZodTuple< - [] | [z.ZodType, ...(z.ZodType | z.ZodUnknown)[]], - z.ZodType | z.ZodUnknown | null - >, - Result extends z.ZodType | z.ZodUndefined, -> = (zod: typeof z) => z.ZodFunction; - /** @alpha */ export type TemplateGlobalFunctionExample = { description?: string; @@ -44,31 +35,17 @@ export type TemplateGlobalFunctionExample = { /** @alpha */ export type CreatedTemplateGlobalFunction< - TSchema extends - | TemplateGlobalFunctionSchema - | undefined - | unknown = unknown, - TFilterSchema extends TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : Exclude< - TemplateGlobal, - JsonValue - > = TSchema extends TemplateGlobalFunctionSchema - ? z.infer> - : TSchema extends unknown - ? unknown - : Exclude, + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, > = { id: string; description?: string; examples?: TemplateGlobalFunctionExample[]; - schema?: TSchema; - fn: TFilterSchema; + schema?: ZodFunctionSchema; + fn: (...args: z.infer>) => z.infer; }; /** @alpha */ export type CreatedTemplateGlobal = | CreatedTemplateGlobalValue - | CreatedTemplateGlobalFunction; + | CreatedTemplateGlobalFunction; diff --git a/plugins/scaffolder-node/src/alpha/index.ts b/plugins/scaffolder-node/src/alpha/index.ts index d97aa54557..9df968a52d 100644 --- a/plugins/scaffolder-node/src/alpha/index.ts +++ b/plugins/scaffolder-node/src/alpha/index.ts @@ -27,6 +27,7 @@ import { CreatedTemplateGlobal } from './globals'; export * from '../tasks/alpha'; export * from './filters'; export * from './globals'; +export * from './types'; /** * Extension point for managing scaffolder actions. @@ -73,7 +74,7 @@ export const scaffolderTaskBrokerExtensionPoint = */ export interface ScaffolderTemplatingExtensionPoint { addTemplateFilters( - filters: Record | CreatedTemplateFilter[], + filters: Record | CreatedTemplateFilter[], ): void; addTemplateGlobals( diff --git a/plugins/scaffolder-node/src/alpha/types.ts b/plugins/scaffolder-node/src/alpha/types.ts new file mode 100644 index 0000000000..ca52973775 --- /dev/null +++ b/plugins/scaffolder-node/src/alpha/types.ts @@ -0,0 +1,30 @@ +/* + * 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 { z } from 'zod'; + +/** + * @alpha + */ +export type ZodFunctionSchema< + TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], + TReturnType extends z.ZodTypeAny, +> = ( + zod: typeof z, +) => + | z.ZodFunction, TReturnType> + | z.ZodType< + (...args: z.infer>) => z.infer + >;