diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md index a95cee24f4..50495b1784 100644 --- a/docs/features/software-templates/input-examples.md +++ b/docs/features/software-templates/input-examples.md @@ -181,9 +181,11 @@ parameters: ## Use parameters as condition in steps +Conditions use Javascript equality operators. + ```yaml - name: Only development environments - if: ${{ parameters.environment === "staging" and parameters.environment === "development" }} + if: ${{ parameters.environment === "staging" or parameters.environment === "development" }} action: debug:log input: message: 'development step' @@ -193,6 +195,12 @@ parameters: action: debug:log input: message: 'production step' + +- name: Non-production environments + if: ${{ parameters.environment !== "prod" and parameters.environment !== "production" }} + action: debug:log + input: + message: 'non-production step' ``` ## Use parameters as conditional for fields @@ -218,10 +226,15 @@ parameters: lastName: title: Last Name type: string + # You can use additional fields of parameters within conditional parameters such as required. + required: + - lastName ``` ## Conditionally set parameters +The `if` keyword within the parameter uses [nunjucks templating](https://mozilla.github.io/nunjucks/templating.html#if). The `not` keyword is unavailable; instead, use javascript equality. + ```yaml spec: parameters: @@ -237,4 +250,9 @@ spec: action: fetch:template input: url: ${{ parameters.path if parameters.path else '/root' }} + - id: fetch_not_example + name: Fetch template not example + action: fetch:template + input: + url: ${{ '/root' if parameters.path !== true else parameters.path }} ``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index dab08e3454..dd5667b7b1 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -36,6 +36,7 @@ import { z } from 'zod'; export const createNewFileAction = () => { return createTemplateAction({ id: 'acme:file:create', + description: 'Create an Acme file.', schema: { input: z.object({ contents: z.string().describe('The contents of the file'), @@ -63,9 +64,10 @@ for reference. The `createTemplateAction` takes an object which specifies the following: -- `id` - a unique ID for your custom action. We encourage you to namespace these +- `id` - A unique ID for your custom action. We encourage you to namespace these in some way so that they won't collide with future built-in actions that we may ship with the `scaffolder-backend` plugin. +- `description` - An optional field to describe the purpose of the action. This will populate in the `/create/actions` endpoint. - `schema.input` - A `zod` or JSON schema object for input values to your function - `schema.output` - A `zod` or JSON schema object for values which are output from the function using `ctx.output` @@ -80,6 +82,7 @@ import { writeFile } from 'fs'; export const createNewFileAction = () => { return createTemplateAction<{ contents: string; filename: string }>({ id: 'acme:file:create', + description: 'Create an Acme file.', schema: { input: { required: ['contents', 'filename'], @@ -118,7 +121,7 @@ We follow `provider:entity:verb` or as close to this as possible for our built i Also feel free to use your company name to namespace them if you prefer too, for example `acme:file:create` like above. -Prefer to use `camelCase` over `snake-case` for these actions if possible, which leads to better reading and writing of template entity definitions. +Prefer to use `camelCase` over `snake_case` or `kebab-case` for these actions if possible, which leads to better reading and writing of template entity definitions. > We're aware that there are some exceptions to this, but try to follow as close as possible. We'll be working on migrating these in the repository over time too. @@ -143,6 +146,65 @@ argument. It looks like the following: ## Registering Custom Actions +To register your new custom action in the Backend System you will need to create a backend module. Here is a very simplified example of how to do that: + +```ts title="packages/backend/src/index.ts" +/* highlight-add-start */ +import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +/* highlight-add-end */ + +/* highlight-add-start */ +const scaffolderModuleCustomExtensions = createBackendModule({ + pluginId: 'scaffolder', // name of the plugin that the module is targeting + moduleId: 'custom-extensions', + register(env) { + env.registerInit({ + deps: { + scaffolder: scaffolderActionsExtensionPoint, + // ... and other dependencies as needed + }, + async init({ scaffolder /* ..., other dependencies */ }) { + // Here you have the opportunity to interact with the extension + // point before the plugin itself gets instantiated + scaffolder.addActions(new createNewFileAction()); // just an example + }, + }); + }, +}); +/* highlight-add-end */ + +const backend = createBackend(); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); +/* highlight-add-next-line */ +backend.add(scaffolderModuleCustomExtensions()); +``` + +If your custom action requires core services such as `config` or `cache` they can be imported in the dependencies and passed to the custom action function. + +```ts title="packages/backend/src/index.ts" +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; + +... + + env.registerInit({ + deps: { + scaffolder: scaffolderActionsExtensionPoint, + cache: coreServices.cache, + config: coreServices.rootConfig, + }, + async init({ scaffolder, cache, config }) { + scaffolder.addActions( + customActionNeedingCacheAndConfig({ cache: cache, config: config }), + ); + }) +``` + +### Register Custom Actions with the Legacy Backend System + Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the `scaffolder-backend` `createRouter` function. You should have something similar to the below in @@ -193,42 +255,6 @@ export default async function createPlugin( } ``` -### Register Action With New Backend System - -To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that: - -```ts title="packages/backend/src/index.ts" -/* highlight-add-start */ -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; -import { createBackendModule } from '@backstage/backend-plugin-api'; -/* highlight-add-end */ - -/* highlight-add-start */ -const scaffolderModuleCustomExtensions = createBackendModule({ - pluginId: 'scaffolder', // name of the plugin that the module is targeting - moduleId: 'custom-extensions', - register(env) { - env.registerInit({ - deps: { - scaffolder: scaffolderActionsExtensionPoint, - // ... and other dependencies as needed - }, - async init({ scaffolder /* ..., other dependencies */ }) { - // Here you have the opportunity to interact with the extension - // point before the plugin itself gets instantiated - scaffolder.addActions(createNewFileAction()); // just an example - }, - }); - }, -}); -/* highlight-add-end */ - -const backend = createBackend(); -backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); -/* highlight-add-next-line */ -backend.add(scaffolderModuleCustomExtensions()); -``` - ## List of custom action packages Here is a list of Open Source custom actions that you can add to your Backstage diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index d1fac931e5..15591eeb67 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -311,10 +311,37 @@ spec: ``` If you have a feature flag `experimental-feature` active then -your first step would be shown. The same goes for the nested properties in the +your first set of parameter fields would be shown. The same goes for the nested properties in the spec. Make sure to use the key `backstage:featureFlag` in your templates if you want to use this functionality. +Feature Flags cannot be used in `spec.steps[].if`(the conditional on whether to execute an step/action). But you can use feature flags to display parameters that allow for skipping steps. + +```yaml +spec: + type: website + owner: team-a + parameters: + - name: Enter some stuff + description: Enter some stuff + backstage:featureFlag: experimental-feature + properties: + skipStep: + type: boolean + title: Whether or not to skip a step. + default: false + restOfParameters: + ... + steps: + - id: skipMe + name: A step to skip if the feature flag is turned on and the user selects true + action: debug:log + if: ${{ parameters.skipStep }} + input: + message: | + ... +``` + ### The Repository Picker In order to make working with repository providers easier, we've built a custom @@ -709,7 +736,7 @@ an entity reference, such as the `kind`, `namespace`, and `name`. ### pick -This `pick` filter allows you to select specific properties from an object. +This `pick` filter allows you to select specific properties (`kind`, `namespace`, `name`) from an object. **Usage Example** diff --git a/docs/features/software-templates/writing-tests-for-actions.md b/docs/features/software-templates/writing-tests-for-actions.md index 0c92811b6f..0bfd97e49f 100644 --- a/docs/features/software-templates/writing-tests-for-actions.md +++ b/docs/features/software-templates/writing-tests-for-actions.md @@ -4,15 +4,13 @@ title: Writing Tests For Actions description: How to write tests for actions --- -Once you created a new action, your own custom one, or you would like to contribute new actions, you have to cover it with -Unit tests to be sure that your actions do what they suppose to do. +# Unit Testing Custom Actions -Make sure that you cover the most of scenario's, which could happen with the action. -One of indispensable part of the test is to supply the context to a handler of action for the execution. -We encourage you to use a utility method for that, so your tests are immune to structural changes of context. -What is inevitably going to happen during the time. +Unit tests help prevent regressions in custom action functionality. The `createTemplateAction` function that is the core of a custom action can be difficult to mock. There are helper methods that can assist. -Example how to use it: +## Mocking the Context + +The `handler` property of the `createTemplateAction` input object expects a context. You can create a mock context using the code below: ```typescript import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -29,10 +27,11 @@ expect(mockContext.output).toHaveBeenCalledWith( ); ``` +### Mocking a Workspace within the Context object + One thing to be aware about: if you would like to call `createMockActionContext` inside `it`, you have to provide a `workspacePath`. By default, `createMockActionContext` uses -`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. -This implementation contains a hook inside which creates this limitation. So in this case you can do then: +`import { createMockDirectory } from '@backstage/backend-test-utils';` to create it for you. You can use the code below to customize the `workspacePath` without using the default workspace of the `createMockActionContext` function. ```typescript describe('github:autolinks:create', async () => { @@ -55,3 +54,63 @@ describe('github:autolinks:create', async () => { }); }); ``` + +## Mocking a Config Core Service + +If your custom Action requires the Config Core Service within execution of the `handler(ctx)` such as the custom action below, mocking the context object can be done by building a `mockContext` with the `ConfigReader` function within the `@backstage/config` package. + +```typescript +// custom-action.ts +import { Config } from '@backstage/config'; + +export const customActionRequiringConfigCoreService = (config: Config) => { + const fieldRequiringValueFromConfig = config.getString('app.service.url'); + return createTemplateAction({ + ... + async handler(ctx) { + // Some code requiring the config const + ctx.logger.info(fieldRequiringValueFromConfig); + } + }) +} +``` + +```typescript +// custom-action.test.ts +import { ConfigReader } from '@backstage/config'; +import { customActionRequiringConfigCoreService } from './custom-action.ts'; +... +const mockConfig = new ConfigReader({ + app: { + service: { + url: 'https://api.service.io/graphql', + apiKeyId: '123', + apiKeySecret: '123abc', + }, + }, +}); +... +const action = customActionRequiringConfigCoreService(mockConfig); +await action.handler({ + ...mockContext +}) +``` + +## Mocking a Cache Core Service + +Similar to the `Mocking a Config Core Service` section above, if your custom action expects a Cache Core Service Object as part of the function input, you can mock it out with the following: + +```typescript +import { CacheService } from '@backstage/backend-plugin-api'; + +const mockCacheServiceMethods = { + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), +}; + +const mockCacheService = mockCacheServiceMethods as unknown as CacheService; + +const action = customActionRequiringCacheCoreService(mockCacheService); +... +```